Tuesday, May 31, 2011

iPhone composed video preview in portrait/landscape how-to




Abstract: I address how to preview / generate thumbnail images / export a composed movie in an appropriate video orientation (portrait or landscape). For example, previewing a movie of iPod library using AVPlayer works well with any kind of its video orientation. However, once I compos the movie, its video orientation is lost in the composed movie, so AVPlayer can not show it in an adequate orientation. I demonstrate a sample application which show a movie in right orientation using a layer instruction. You can get its source code from github : https://github.com/reinforce-lab/ObjC_Codes/tree/master/moviePortraitLandscape under the New BSD license.
Introduction
I have been developing an iPhone movie editor for a month. Video composition is somehow complicated but it is not difficult if you watch WWDC 2010 “session 407 Editing Media with AV Foundation” and read thorough its sample project “AVEditDemo” source codes.
However, you have to take care of its video orientation in movie composition by yourself. Movie object is represented as an instance of AVAsset abstract class in iOS SDK. AVAsset abstract class has a property named “CGAffineTransform preferredTransform”. All movie files recorded using the default iPhone camera application has a video frame in landscape (home button is on right hand) and its video orientation can be got from the preferredTransform property of AVAsset. For example, you record a portrait movie (home button is bottom), preferredTransform is set to rotate the video frame 90 degrees to the right clockwise. Therefore when you edit a movie, you also have to set a appropriate transformation same as the original movie has.
AVMutableComposition class which inherits AVAsset is used to a compose movie but its preferredTransform property is read-only , not writable. So you can not copy transformation of a source movie to the composed movie. Creating a new AVMutableComposition inheritance class whose preferredTransform property is writable is not easy, because AVMutableComposition class is merely a wrapper class of its internal class (AVMutableComposition class method is used to instance).

How to set video orientation of a composed movie

Therefore, I rotate video frames itself using AVMutableVideoCompositionLayerInstruction to set video orientation. First I have to determine a video orientation of a source movie as following (avPlayerView.m line #338):
  1. // AVURLAsset *asset;
  2. UIImageOrientation orientation_  = UIImageOrientationUp;
  3. BOOL  isPortrait_  = NO;
  4.  
  5.   NSArray *tracks = [asset tracksWithMediaType:AVMediaTypeVideo];
  6.   if([tracks  count] != 0) {
  7.     AVAssetTrack *videoTrack = [tracks objectAtIndex:0];
  8.     CGAffineTransform t = videoTrack.preferredTransform;
  9.     if(t.== 0 && t.== 1.0 && t.== -1.0 && t.== 0)  {orientation_= UIImageOrientationRight; isPortrait_ = YES;}
  10.     if(t.== 0 && t.== -1.0 && t.== 1.0 && t.== 0)  {orientation_ =  UIImageOrientationLeft; isPortrait_ = YES;}
  11.     if(t.== 1.0 && t.== 0 && t.== 0 && t.== 1.0)   {orientation_ =  UIImageOrientationUp;}
  12.     if(t.== -1.0 && t.== 0 && t.== 0 && t.== -1.0) {orientation_ = UIImageOrientationDown;} }
As addressed before, movie is represented as a AVURLAsset instance (line #1). Video orientation which determined in line #9-12 is stored in the preferredTransform property of a video track (line #8).
Video frame rotation is set as flowing: (avPlayerView.m line #444)
  1. AVAssetTrack *videoTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
  2. AVMutableComposition *composition = [AVMutableComposition composition];
  3. AVMutableVideoComposition *videoComposition = [AVMutableVideoComposition videoComposition];
  4.  
  5. // get frame size. rotate the source frame rectangle if it is portrait.
  6. CGSize videoSize = videoTrack.naturalSize;
  7. if(isPortrait_) {    videoSize = CGSizeMake(videoSize.height, videoSize.width);  }
  8.  
  9.   // set frame size
  10. composition.naturalSize     = videoSize; // AVPlayerはこのサイズを見ない
  11. videoComposition.renderSize = videoSize; // AVPlayerはこのサイズを見る。
  12. videoComposition.frameDuration= CMTimeMakeWithSeconds( 1 / videoTrack.nominalFrameRate, 600);
  13.  
  14. // Trackを構築
  15. AVMutableCompositionTrack * compositionVideoTrack = [composition
  16.                addMutableTrackWithMediaType:AVMediaTypeVideo
  17.                preferredTrackID:kCMPersistentTrackID_Invalid]; 
  18.   [compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration)
  19.                    ofTrack:videoTrack    
  20.                   atTime:kCMTimeZero error:nil];
  21.  
  22.   // レイヤを設定
  23.   AVMutableVideoCompositionLayerInstruction *layerInst;
  24.   layerInst = [AVMutableVideoCompositionLayerInstruction
  25. videoCompositionLayerInstructionWithAssetTrack:videoTrack];
  26. // ここがポイント! 変形を指定
  27.   [layerInst setTransform:videoTrack.preferredTransform atTime:kCMTimeZero];
  28.  // Layer instructionを設定
  29. AVMutableVideoCompositionInstruction *inst = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
  30. inst.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
  31. inst.layerInstructions = [NSArray arrayWithObject:layerInst];
  32. videoComposition.instructions = [NSArray arrayWithObject:inst];
  33. // プレビュ表示
  34. AVPlayerItem *playerItem_ = [[[AVPlayerItem alloc] initWithAsset:composition] autorelease];
  35. playerItem_.videoComposition = videoComposition;
  36. // AVPlayer player_
  37.  [player_ replaceCurrentItemWithPlayerItem:playerItem_];
First, I instance AVMutableComposition and VideoComposition (line #1-3), setting frame and render size (#5-12) and then store the source video track into the composition (#14-20). After that, I instance a AVMutalbeVideoCompositonLayerInstruction and set preferredTransform of the source video to it (#23-32). At last, set the composed video to preview (#34-38). These usual video composition code.

Thumbnail image generation and exporting movie

This technique is also used for thumbnail image generation and exporting movie.
AVAssetImageGenerator can be used to generate multiple movie thumbnail images asynchronously. When I set its BOOL appliesPreferredTrackTransform property YES, however, image generation fails when video orientation is not landscape home right. Therefore I set the property NO and rotate the generate thumbnail images.
AVAssetExportSession can be used to export the composed asset to a local file. You can export a movie which is exactly you see in the preview. However, if you want to keep the consistency in video orientation policy (frame is always landscape right home, and video orientation is set in the preferredTransform property), you can do it by writing a custom export class (set transform property of AVAssetWriterInput).

Conclusion

I demonstrate previewing a composed asset in an adequate orientation. Although using LayerInstruction, AVPlayer can not show the movie when AVPlayerLayer videoGravity property is AspectFill, may be AVPlayerLayer does not look into the layer instructions, so it can not determine adequate layer size. So if you want to preview in AspectFill, you have to set appropriate render size and a transform by yourself.

Monday, October 4, 2010

iPhone_GettingPowerFromAudioPlug

Abstract: iPhone dock connector can not be used unless signing a nondisclosure agreement with Apple. So, audio interface is the only way to connect the external hardware to iPhone without Jail Break. Here I show I-V characteristics of the audio interface as a power supply. Audio sinusoidal signal rectified using silicone diode voltage doubler[1] gives open voltage 1.5 V and it drops to 1.2 V when output current is 1 mA. Its large voltage loss dues to silicone diode's large forward voltage. Microphone terminal also can be used as a power source. Its output voltage is 2.7 V and output resistance is 2.3 kohm. The amount of power is small but enough to use a ATMEL AVR low-power microprocessor which requires current 0.3 mA/MHz.
 Sound software modem projects have been developed to make an iPhone accessory easily [2](in Japanese). An external hardware can send or receive data to iPhone by modulated audio signal via audio interface. Here I tried to supply power via audio interface to eliminate the need for battery of the external hardware.

Audio output as a power source
 iPhone headphone jack has two audio output channels. Its output impedance is 36 ohm and maximum voltage amplitude is 0.95 Vrms (2.7 V peak-to-peak). Therefore one can get up to 6 mW per channel.
Fig. 1: Measurement circuit.

 To get enough output voltage for a logic circuit (1.8 V or 3.3 V), a measurement circuit shown in Fig.1 is a simple voltage doubler [1]. Silicon planar diode, 1S1588 which is popular in japan, is used.


Fig. 2: Measured output current-voltage.
 Measurements result is shown in Fig.2. Audio waveform is a sinusoidal and its frequency is 15 kHz generated by the sample code [3]. Output volume is maximum. The output voltage is 1.5 V when Iout is zero. Its voltage dorp (2.7 Vpk-pk - 1.5 V = 1.2 V) is that of silicon diode forward voltage drop (0.6 V). As Fig.2 shows, the output voltage increases by only 50 mV even when C1 is changed from 4.7 uF to 330uF. The output voltage drop is caused by the diode forward voltage drop.
 If the silicon diodes are replaced with schottky barrier diodes, output voltage will be 1.8 V at 1 mA output (if diode forward voltage is 0.4 V).

Microphone terminal as a power source
 Microphone terminal of iPhone tetrapolar audio jack is biased to drive a capacitor microphone. Its output voltage-current measurement result is shown in Fig.3. Open-circuit output voltage is 2.7 V and its output impedance is 2.3 kohm.
Fig.3 Measured output current-voltage of a microphone terminal.

Discussion

 How to get power source from an iPhone headphone jack is described. Audio terminal gives 1.8 V at 1 mA, and microphone 1.8V at 300uA. It is enough for a low-power microphocessor. Higher output voltage (up to 4.5 V) can be get using L and R-channel reverse phase signal.

References
  1. Wikipedia: Voltage doubler, http://en.wikipedia.org/wiki/Voltage_doubler
  2. Sound modem projects (in Japanese), http://www.reinforce-lab.com/projects/iphone-software-modem
  3. MonoTouch Audio Unit sample code, http://blog.reinforce-lab.com/2010/08/monotouchaudiounitsamplecodes.html


Monday, August 2, 2010

MonoTouch_AudioUnit_SampleCodes

Abstract:I demonstrate three AudioUnit sample codes using MonoTouch: sine waveform generator, AIFF audio file player, and monitoring microphone. Comparing to other frameworks (Audio Queue Service and OpenAL), AudioUnit can realize real-time audio rendering and the lowest output latency (~ 3 msec), which is suitable for instrument or sound-triggerd application. The sample codes are in the github under the MIT license so that you can use it freely [1].

 In my software modem project, I once decided to use AudioQueue Serivce to access speaker and microphone. But I need smaller output latency than 23 msec, which is the lowest achievable value with Audio Queue Service. Therefore I try to use AudioUnit which is the lowest-level audio API where its output latency is up to 3 msec.

MonoTouch AudioUnit marshaling

 AudioUnit is a set of C language methods included in AudioToolbox framework.Therefore interoperating with unmanaged code is required to use it from MonoTouch [2]. Details of the interoperation is described in the wrapper classe source codes (a project MonoTouch.AudioUnit in the sample code) which correspond to AudioUnit method and struct definitions one by one.

How to output an arbitrary signal using audio unit
 Arbitrary signal generation code using Audio Unit is basically the same to that of Audio Queue Service described in my previous article ( http://blog.reinforce-lab.com/2010/05/monotouchaudioqueuetonegenerator.html ).
To output arbitrary signal:
  1. Setting up output audio unit,
  2. Setting an event handler to render,
  3. Filling waveform in the buffer.

 Procedure to create output audio unit (Remote IO unit) is already described in Apple developer's documents. First, output audio unit (Remote IO unit) is created as the following code:
   1:  void prepareAudioUnit()
   2:  {
   3:      // Creating AudioComponentDescription instance of RemoteIO Audio Unit
   4:      AudioComponentDescription cd = new AudioComponentDescription()
   5:      {
   6:          componentType    = AudioComponentDescription.AudioComponentType.kAudioUnitType_Output,
   7:          componentSubType = AudioComponentDescription.AudioComponentSubType.kAudioUnitSubType_RemoteIO,
   8:          componentManufacturer = AudioComponentDescription.AudioComponentManufacturerType.kAudioUnitManufacturer_Apple,
   9:          componentFlags = 0,
  10:          componentFlagsMask = 0
  11:      };
  12:      
  13:      // Getting AudioComponent from the description
  14:      _component = AudioComponent.FindComponent(cd);
  15:     
  16:      // Getting Audiounit
  17:      _audioUnit = AudioUnit.CreateInstance(_component);
  18:   
  19:      // setting AudioStreamBasicDescription
  20:      int AudioUnitSampleTypeSize;
  21:      if (MonoTouch.ObjCRuntime.Runtime.Arch == MonoTouch.ObjCRuntime.Arch.SIMULATOR)
  22:      {
  23:          AudioUnitSampleTypeSize = sizeof(float);
  24:      }
  25:      else
  26:      {
  27:          AudioUnitSampleTypeSize = sizeof(int);
  28:      }
  29:      AudioStreamBasicDescription audioFormat = new AudioStreamBasicDescription()
  30:      {
  31:          SampleRate = _sampleRate,
  32:          Format = AudioFormatType.LinearPCM,
  33:          //kAudioFormatFlagsAudioUnitCanonical = kAudioFormatFlagIsSignedInteger | kAudioFormatFlagsNativeEndian | kAudioFormatFlagIsPacked | kAudioFormatFlagIsNonInterleaved | (kAudioUnitSampleFractionBits << kLinearPCMFormatFlagsSampleFractionShift),
  34:          FormatFlags = (AudioFormatFlags)((int)AudioFormatFlags.IsSignedInteger | (int)AudioFormatFlags.IsPacked | (int)AudioFormatFlags.IsNonInterleaved | (int)(kAudioUnitSampleFractionBits << (int)AudioFormatFlags.LinearPCMSampleFractionShift)),
  35:          ChannelsPerFrame = 2,
  36:          BytesPerPacket = AudioUnitSampleTypeSize,
  37:          BytesPerFrame = AudioUnitSampleTypeSize,
  38:          FramesPerPacket = 1,
  39:          BitsPerChannel = 8 * AudioUnitSampleTypeSize,
  40:          Reserved = 0
  41:      };
  42:      _audioUnit.SetAudioFormat(audioFormat, AudioUnit.AudioUnitScopeType.kAudioUnitScope_Input, 0);            
  43:   
  44:      // setting callback
  45:      _audioUnit.RenderCallback += new EventHandler<AudioUnitEventArgs>(callback);
  46:  }

 Because Audio unit sample type is different between a simulator (32-bit float) and a device (32-bit int), the difference is absorbed in the code (line #21-28). Each time audio unit requires buffer rendering, a render callback event handler (line #45) is invoked as following:

   1:  void callback(object sender, AudioUnitEventArgs args)
   2:  { 
   3:      // Generating sin waveform
   4:      double dphai = 440 * 2.0 * Math.PI / _sampleRate;
   5:      
   6:      // Getting a pointer to a buffer to be filled
   7:      IntPtr outL = args.Data.mBuffers[0].mData;            
   8:      IntPtr outR = args.Data.mBuffers[1].mData;
   9:      
  10:      // filling sin waveform.
  11:      // AudioUnitSampleType is different between a simulator (float32) and a real device (int32).
  12:      if (MonoTouch.ObjCRuntime.Runtime.Arch == MonoTouch.ObjCRuntime.Arch.SIMULATOR)
  13:      {
  14:          unsafe
  15:          {
  16:              var outLPtr = (float *) outL.ToPointer();
  17:              var outRPtr = (float *) outR.ToPointer();
  18:              for (int i = 0; i < args.NumberFrames; i++)
  19:              {                        
  20:                  float sample = (float)Math.Sin(_phase) / 2048;
  21:                  *outLPtr++ = sample;
  22:                  *outRPtr++ = sample;
  23:                  _phase += dphai;
  24:              }
  25:          }
  26:      }
  27:      else
  28:      {
  29:          unsafe 
  30:          {
  31:              var outLPtr = (int*)outL.ToPointer();
  32:              var outRPtr = (int*)outR.ToPointer();
  33:              for (int i = 0; i < args.NumberFrames; i++)
  34:              {                        
  35:                  int sample = (int)(Math.Sin(_phase) * int.MaxValue / 128); // signal waveform format is fixed-point (8.24)
  36:                  *outLPtr++ = sample;
  37:                  *outRPtr++ = sample;
  38:                  _phase += dphai;
  39:              }
  40:          }
  41:      }
  42:   
  43:      _phase %= 2 * Math.PI;
  44:  }

 Audio buffer is a 32-bit float or 32-bit integer array. Therefore arbitrary waveform can be written in the buffer within unsafe block as above. Array length can be set using AudioSesion framework, whose default value is 512 and must be more than 128. Smaller array length can achieve less output latency while it needs more processor power.


Other examples
 I omit explanation of other examples, playing audio file and recording from the microphone. Please read the source code :-P

References

  1. Sample codes, http://github.com/reinforce-lab/MonoTouch.AudioUnit
  2. Interoperating with unmanaged code, http://msdn.microsoft.com/en-us/library/sd10k43k.aspx


Tuesday, June 15, 2010

C#_HowTo_Convert_PDF_into_Images

Abstract: I demonstrate a C# code to convert a PDF file into image files directly without using a 3rd party library. I adopt an open-source virtual image printer driver and this C# example codes invokes Adobe AcrobatReader to print out to it.


How can I covert PDF into image files?

 There are several commercial PDF conversion libraries, but no open-source library. Concrete methods are: 1) an open-source PDF rendering engine such as GhostScript, and 2) a virtual printer. The former method is not so good, because it forces users to install a software and fonts and rendering image may not be the same as Adobe AcrobatReader. The latter method, I have tried to use XPS document writer but it does not work as following:

 XPS Document Writer can convert any printable files into XPS files. However, it shows a modal dialog to ask a user in which folder XPS files should be saved. There are some discussions about how to specify the folder name from code, in Printing documents to Microsoft XPS Document Writer without user interaction - Feng Yuan (袁峰) - Site Home - MSDN Blogs, and Is There Really No Way To Print XPS From A Generic Application Without Dialog Box?. But it is not easy for me ;-P

 Therefore I use an open-source virtual printer here.

Using an open-source virtual printer

 I use Virtual Image Printer driver | Download Virtual Image Printer driver software for free at SourceForge.net. As I address above, a user must install install this driver software ... what else?
 The following code does:
  1. Setting output folder name to the registry,
  2. Invoking AcrobatReader to print out,
  3. Waiting for job finishing.

Setting AcrobatReader executable file path and output folder 

 Strings named pdf_filepath and output_folder in Main method specifies AcrobatReader executable path and image file output folder. The example code uses AcrobatReader8.0:  psInfo.FileName = @"C:\Program Files\Adobe\Reader 8.0\Reader\AcroRd32.exe";.

Output folder is set into registry

 ImagePrinter (virtual printer) holds setting information under the registry "HKEY_LOCAL_MACHINE\Software\ImagePrinter". Output folder name is set to the registry key "path".

A little ingenuity to prevent misspelling

 Misspelling registry key results in a bug difficult to find. Therefore I create a value object then reflect its content to the registry using Sysmte.Reflection.FieldInfo.

AcrobatReader command options

 Detailed descriptions of AcrobatReader command options is here: . Options "/s", "/h", "/t" mean preventing splash window, minimizing window, specifying printer name.

Print queue monitoring 

 To wait finishing print job, first detecting image files are in the folder, next waiting while printer queue is not empty.

Example code

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:   
   5:  using System.Runtime.InteropServices;
   6:  using System.ComponentModel;
   7:   
   8:  namespace pdf2image
   9:  {
  10:      /// <summary>
  11:      /// Converting a PDF file to image files
  12:      /// 2008/07/12 Akihiro Uehara    
  13:      /// </summary>    
  14:   
  15:      // Preparation
  16:      // Install printer driver http://sourceforge.net/projects/imageprinter
  17:   
  18:      public class Program
  19:      {
  20:          #region printer setting value object
  21:          class printer_configration
  22:          {
  23:              public string ext_app;
  24:              public string format;
  25:              public string format_ext;
  26:              public string multipage_tiff;
  27:              public string one_file;
  28:              public string original_name;
  29:              public string path;
  30:              public string q_jpg;
  31:              public string ShowProgress;
  32:   
  33:              const string _registry_prefix = @"Software\ImagePrinter\";
  34:   
  35:              public printer_configration()
  36:              { 
  37:                  ReadRegistry();
  38:              }
  39:   
  40:              void write_registry(string key, string value)
  41:              {
  42:                  if (null == value)
  43:                      return;
  44:   
  45:                  // Writing to registry "HKEY_LOCAL_MACHINE\Software\ImagePrinter\" 
  46:                  Microsoft.Win32.RegistryKey regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(_registry_prefix, true);
  47:                  if (null != regkey)
  48:                  {
  49:                      regkey.SetValue(key, value);
  50:                      regkey.Close();
  51:                  }
  52:              }
  53:   
  54:              string read_registry(string key)
  55:              {
  56:                  string val = string.Empty;
  57:                  // Writing to the registry "HKEY_LOCAL_MACHINE\Software\ImagePrinter\"
  58:                  Microsoft.Win32.RegistryKey regkey = Microsoft.Win32.Registry.LocalMachine.OpenSubKey(_registry_prefix, false);
  59:                  if (null != regkey)
  60:                  {
  61:                      val = (string)regkey.GetValue(key);
  62:                      regkey.Close();
  63:                  }
  64:                  
  65:                  return val;
  66:              }
  67:   
  68:              public void ReadRegistry()
  69:              {
  70:                  // the following code is equivalent to:
  71:                  /*
  72:                  ext_app             = read_registry("ext_app");
  73:                  format              = read_registry("format");
  74:                  format_ext          = read_registry("format_ext");
  75:                  multiplepage_tiff   = read_registry("multiplepage_tiff");
  76:                  one_file            = read_registry("one_file");
  77:                  original_name       = read_registry("original_name");
  78:                  path                = read_registry("path");
  79:                  q_jpg               = read_registry("q_jpg");
  80:                  ShowProgress        = read_registry("ShowProgress");
  81:                  */
  82:                  
  83:                  // To prevend misspelling, fileds of the value object has the name which is the same to the registry key name.
  84:                  foreach (System.Reflection.FieldInfo finfo in typeof(printer_configration).GetFields())
  85:                      finfo.SetValue(this, read_registry(finfo.Name));
  86:              }
  87:   
  88:              public void WriteRegistry()
  89:              {
  90:                  foreach (System.Reflection.FieldInfo finfo in typeof(printer_configration).GetFields())
  91:                      write_registry(finfo.Name, (string)finfo.GetValue(this));
  92:              }
  93:          }
  94:          #endregion
  95:   
  96:          static bool imageFileExists(string folder, string keyphrase)
  97:          {
  98:              string [] files = System.IO.Directory.GetFiles(folder);
  99:              return Array.Exists<string>(files, delegate(string item) 
 100:              {
 101:                  string fname =System.IO.Path.GetFileName(item);
 102:                  return keyphrase != fname && fname.StartsWith(keyphrase); });
 103:          }
 104:   
 105:          static void Main(string[] args)
 106:          {
 107:              string pdf_filepath = @"c:\tmp\test.pdf"; // 変換するPDFファイル
 108:              string output_folder = @"c:\tmp\";
 109:   
 110:              // preserving current printer settings
 111:              printer_configration prevCfg = new printer_configration();
 112:              printer_configration curCfg  = new printer_configration();
 113:              // printer setting
 114:              curCfg.path          = output_folder;  // image output folder
 115:              curCfg.ext_app       = @"";
 116:              curCfg.format        = "png";       // file format is png, due to its smaller file size compared to TIFF (about 1/2 = 1/3)
 117:              curCfg.one_file      = "false";
 118:              curCfg.original_name = "true";      // image file name is the same ot the PDF filename
 119:              curCfg.ShowProgress  = "false";     // does not show progress bar
 120:   
 121:              Console.WriteLine("Setting printer");
 122:              curCfg.WriteRegistry();
 123:   
 124:              // Printing PDF file
 125:              Console.WriteLine("Starting Acrobat reader");
 126:              System.Diagnostics.ProcessStartInfo psInfo = new System.Diagnostics.ProcessStartInfo();
 127:              psInfo.FileName = @"C:\Program Files\Adobe\Reader 8.0\Reader\AcroRd32.exe";
 128:              psInfo.Arguments = String.Format(@" /s /h /t {0} ImagePrinter",pdf_filepath); // ファイル名は適切なPDFファイルを指定,オプション詳細は http://scripting.cocolog-nifty.com/blog/2006/12/pdf_4c95.html を参照.
 129:              psInfo.CreateNoWindow = true; // not to open console window
 130:              psInfo.UseShellExecute = false; // does not use shell
 131:              System.Diagnostics.Process ps = new System.Diagnostics.Process();
 132:              ps.StartInfo = psInfo;
 133:              ps.Start();            
 134:   
 135:              // Waiting for printing
 136:              // First, waiting for the first image file
 137:              Console.WriteLine("First image file has been generated");
 138:              string fname = System.IO.Path.GetFileName(pdf_filepath);
 139:              while (! imageFileExists(output_folder, fname))
 140:                  System.Threading.Thread.Sleep(1000);
 141:              
 142:              // Next, waiting the job queue becomes empty
 143:              Console.WriteLine("Waiting for Job queue empty");
 144:              PRINTER_INFO_2 pinfo;
 145:              do
 146:              {
 147:                  System.Threading.Thread.Sleep(1000);   
 148:                 pinfo = GetPrinterInfo("ImagePrinter"); // polling printer queue
 149:              } while (pinfo.cJobs > 0);
 150:              // If you use .net framework 2.0 later, use System.Printing namespace.
 151:              /*
 152:              System.Printing.LocalPrintServer prtSrv = new System.Printing.LocalPrintServer();
 153:              System.Printing.PrintQueue queue  = prtSrv.GetPrintQueue("ImagePrinter");
 154:              do
 155:              {
 156:                  System.Threading.Thread.Sleep(1000);   // 
 157:                  queue.Refresh();//  checking queue
 158:              } while (queue.NumberOfJobs > 0);
 159:              */ 
 160:              //ps.Kill(); // kill Acrobat reader
 161:   
 162:              // Recovering printer setting
 163:              Console.WriteLine("Recoverring printer setting");
 164:              prevCfg.WriteRegistry();
 165:          }
 166:   
 167:          #region Polling printer port. ref: DOBON.NET http://dobon.net/vb/dotnet/graphics/printerport.html 
 168:   
 169:          //using System.Runtime.InteropServices;
 170:          //using System.ComponentModel;
 171:   
 172:          [DllImport("winspool.drv", CharSet = CharSet.Auto, SetLastError = true)]
 173:          private static extern bool OpenPrinter(string pPrinterName,
 174:              out IntPtr hPrinter, IntPtr pDefault);
 175:   
 176:          [DllImport("winspool.drv", SetLastError = true)]
 177:          private static extern bool ClosePrinter(IntPtr hPrinter);
 178:   
 179:          [DllImport("winspool.drv", SetLastError = true)]
 180:          private static extern bool GetPrinter(IntPtr hPrinter,
 181:              int dwLevel, IntPtr pPrinter, int cbBuf, out int pcbNeeded);
 182:   
 183:          [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
 184:          public struct PRINTER_INFO_2
 185:          {
 186:              public string pServerName;
 187:              public string pPrinterName;
 188:              public string pShareName;
 189:              public string pPortName;
 190:              public string pDriverName;
 191:              public string pComment;
 192:              public string pLocation;
 193:              public IntPtr pDevMode;
 194:              public string pSepFile;
 195:              public string pPrintProcessor;
 196:              public string pDatatype;
 197:              public string pParameters;
 198:              public IntPtr pSecurityDescriptor;
 199:              public uint Attributes;
 200:              public uint Priority;
 201:              public uint DefaultPriority;
 202:              public uint StartTime;
 203:              public uint UntilTime;
 204:              public uint Status;
 205:              public uint cJobs;
 206:              public uint AveragePPM;
 207:          }
 208:   
 209:          /// <summary>
 210:          /// プリンタの情報をPRINTER_INFO_2で取得する
 211:          /// </summary>
 212:          /// <param name="printerName">プリンタ名</param>
 213:          /// <returns>プリンタの情報</returns>
 214:          public static PRINTER_INFO_2 GetPrinterInfo(string printerName)
 215:          {
 216:              //プリンタのハンドルを取得する
 217:              IntPtr hPrinter;
 218:              if (!OpenPrinter(printerName, out hPrinter, IntPtr.Zero))
 219:              {
 220:                  throw new Win32Exception(Marshal.GetLastWin32Error());
 221:              }
 222:   
 223:              IntPtr pPrinterInfo = IntPtr.Zero;
 224:              try
 225:              {
 226:                  //必要なバイト数を取得する
 227:                  int needed;
 228:                  GetPrinter(hPrinter, 2, IntPtr.Zero, 0, out needed);
 229:                  if (needed <= 0)
 230:                      throw new Exception("失敗しました。");
 231:   
 232:                  //メモリを割り当てる
 233:                  pPrinterInfo = Marshal.AllocHGlobal(needed);
 234:   
 235:                  //プリンタ情報を取得する
 236:                  int temp;
 237:                  if (!GetPrinter(hPrinter, 2, pPrinterInfo, needed, out temp))
 238:                  {
 239:                      throw new Win32Exception(Marshal.GetLastWin32Error());
 240:                  }
 241:   
 242:                  //PRINTER_INFO_2型にマーシャリングする
 243:                  PRINTER_INFO_2 printerInfo =
 244:                      (PRINTER_INFO_2)Marshal.PtrToStructure(pPrinterInfo,
 245:                      typeof(PRINTER_INFO_2));
 246:   
 247:                  //結果を返す
 248:                  return printerInfo;
 249:              }
 250:              finally
 251:              {
 252:                  //後始末をする
 253:                  ClosePrinter(hPrinter);
 254:                  Marshal.FreeHGlobal(pPrinterInfo);
 255:              }
 256:          }
 257:          #endregion
 258:      }    
 259:  }