OSDN Git Service

c32a120369db8e2358a488cb8126f4df88335bd5
[handbrake-jp/handbrake-jp-git.git] / macosx / PictureController.mm
1 /* $Id: PictureController.mm,v 1.11 2005/08/01 15:10:44 titer Exp $
2
3    This file is part of the HandBrake source code.
4    Homepage: <http://handbrake.fr/>.
5    It may be used under the terms of the GNU General Public License. */
6
7 #import "PictureController.h"
8
9 @interface PictureController (Private)
10
11 - (NSSize)optimalViewSizeForImageSize: (NSSize)imageSize;
12 - (void)resizeSheetForViewSize: (NSSize)viewSize;
13 - (void)setViewSize: (NSSize)viewSize;
14 - (BOOL)viewNeedsToResizeToSize: (NSSize)newSize;
15
16 @end
17
18 @implementation PictureController
19
20 - (id)initWithDelegate:(id)del
21 {
22         if (self = [super initWithWindowNibName:@"PictureSettings"])
23         {
24         // NSWindowController likes to lazily load its window. However since
25         // this controller tries to set all sorts of outlets before the window
26         // is displayed, we need it to load immediately. The correct way to do
27         // this, according to the documentation, is simply to invoke the window
28         // getter once.
29         //
30         // If/when we switch a lot of this stuff to bindings, this can probably
31         // go away.
32         [self window];
33
34                 delegate = del;
35         fPicturePreviews = [[NSMutableDictionary dictionaryWithCapacity: HB_NUM_HBLIB_PICTURES] retain];
36         }
37         return self;
38 }
39
40 - (void) dealloc
41 {
42     [fPicturePreviews release];
43     [super dealloc];
44 }
45
46 - (void) SetHandle: (hb_handle_t *) handle
47 {
48     fHandle = handle;
49
50     [fWidthStepper  setValueWraps: NO];
51     [fWidthStepper  setIncrement: 16];
52     [fWidthStepper  setMinValue: 64];
53     [fHeightStepper setValueWraps: NO];
54     [fHeightStepper setIncrement: 16];
55     [fHeightStepper setMinValue: 64];
56
57     [fCropTopStepper    setIncrement: 2];
58     [fCropTopStepper    setMinValue:  0];
59     [fCropBottomStepper setIncrement: 2];
60     [fCropBottomStepper setMinValue:  0];
61     [fCropLeftStepper   setIncrement: 2];
62     [fCropLeftStepper   setMinValue:  0];
63     [fCropRightStepper  setIncrement: 2];
64     [fCropRightStepper  setMinValue:  0];
65 }
66
67 - (void) SetTitle: (hb_title_t *) title
68 {
69     hb_job_t * job = title->job;
70
71     fTitle = title;
72
73     [fWidthStepper      setMaxValue: title->width];
74     [fWidthStepper      setIntValue: job->width];
75     [fWidthField        setIntValue: job->width];
76     [fHeightStepper     setMaxValue: title->height];
77     [fHeightStepper     setIntValue: job->height];
78     [fHeightField       setIntValue: job->height];
79     [fRatioCheck        setState:    job->keep_ratio ? NSOnState : NSOffState];
80     [fCropTopStepper    setMaxValue: title->height/2-2];
81     [fCropBottomStepper setMaxValue: title->height/2-2];
82     [fCropLeftStepper   setMaxValue: title->width/2-2];
83     [fCropRightStepper  setMaxValue: title->width/2-2];
84
85     /* Populate the Anamorphic NSPopUp button here */
86     [fAnamorphicPopUp removeAllItems];
87     [fAnamorphicPopUp addItemWithTitle: @"None"];
88     [fAnamorphicPopUp addItemWithTitle: @"Strict"];
89     if (allowLooseAnamorphic)
90     {
91     [fAnamorphicPopUp addItemWithTitle: @"Loose"];
92     }
93     [fAnamorphicPopUp selectItemAtIndex: job->pixel_ratio];
94     
95     /* We initially set the previous state of keep ar to on */
96     keepAspectRatioPreviousState = 1;
97         if (!autoCrop)
98         {
99         [fCropMatrix  selectCellAtRow: 1 column:0];
100         /* If auto, lets set the crop steppers according to current job->crop values */
101         [fCropTopStepper    setIntValue: job->crop[0]];
102         [fCropTopField      setIntValue: job->crop[0]];
103         [fCropBottomStepper setIntValue: job->crop[1]];
104         [fCropBottomField   setIntValue: job->crop[1]];
105         [fCropLeftStepper   setIntValue: job->crop[2]];
106         [fCropLeftField     setIntValue: job->crop[2]];
107         [fCropRightStepper  setIntValue: job->crop[3]];
108         [fCropRightField    setIntValue: job->crop[3]];
109         }
110         else
111         {
112         [fCropMatrix  selectCellAtRow: 0 column:0];
113         }
114         
115         /* Set filters widgets according to the filters struct */
116         [fDetelecineCheck setState:fPictureFilterSettings.detelecine];
117     [fDeinterlacePopUp selectItemAtIndex: fPictureFilterSettings.deinterlace];
118     [fDenoisePopUp selectItemAtIndex: fPictureFilterSettings.denoise];
119     [fDeblockCheck setState: fPictureFilterSettings.deblock];
120     
121     fPicture = 0;
122     MaxOutputWidth = title->width - job->crop[2] - job->crop[3];
123     MaxOutputHeight = title->height - job->crop[0] - job->crop[1];
124     [self SettingsChanged: nil];
125 }
126
127 /* we use this to setup the initial picture filters upon first launch, after that their states
128 are maintained across different sources */
129 - (void) setInitialPictureFilters
130 {
131         /* we use a popup to show the deinterlace settings */
132         [fDeinterlacePopUp removeAllItems];
133     [fDeinterlacePopUp addItemWithTitle: @"None"];
134     [fDeinterlacePopUp addItemWithTitle: @"Fast"];
135     [fDeinterlacePopUp addItemWithTitle: @"Slow"];
136         [fDeinterlacePopUp addItemWithTitle: @"Slower"];
137     
138         /* Set deinterlaces level according to the integer in the main window */
139         [fDeinterlacePopUp selectItemAtIndex: fPictureFilterSettings.deinterlace];
140
141         /* we use a popup to show the denoise settings */
142         [fDenoisePopUp removeAllItems];
143     [fDenoisePopUp addItemWithTitle: @"None"];
144     [fDenoisePopUp addItemWithTitle: @"Weak"];
145         [fDenoisePopUp addItemWithTitle: @"Medium"];
146     [fDenoisePopUp addItemWithTitle: @"Strong"];
147         /* Set denoises level according to the integer in the main window */
148         [fDenoisePopUp selectItemAtIndex: fPictureFilterSettings.denoise];
149     
150     /* we use a popup to show the decomb settings */
151         [fDecombPopUp removeAllItems];
152     [fDecombPopUp addItemWithTitle: @"None"];
153     [fDecombPopUp addItemWithTitle: @"Default"];
154     [fDecombPopUp addItemWithTitle: @"Custom"];
155         /* Set denoises level according to the integer in the main window */
156         [fDecombPopUp selectItemAtIndex: fPictureFilterSettings.decomb];
157
158 }
159
160 // Adjusts the window to draw the current picture (fPicture) adjusting its size as
161 // necessary to display as much of the picture as possible.
162 - (void) displayPreview
163 {
164     [fPictureView setImage: [self imageForPicture: fPicture]];
165         
166         NSSize displaySize = NSMakeSize( ( CGFloat )fTitle->width, ( CGFloat )fTitle->height );
167     /* Set the picture size display fields below the Preview Picture*/
168     if( fTitle->job->pixel_ratio == 1 ) // Original PAR Implementation
169     {
170         output_width = fTitle->width-fTitle->job->crop[2]-fTitle->job->crop[3];
171         output_height = fTitle->height-fTitle->job->crop[0]-fTitle->job->crop[1];
172         display_width = output_width * fTitle->job->pixel_aspect_width / fTitle->job->pixel_aspect_height;
173         [fInfoField setStringValue:[NSString stringWithFormat:
174                                     @"Source: %dx%d, Output: %dx%d, Anamorphic: %dx%d",
175                                     fTitle->width, fTitle->height, output_width, output_height, display_width, output_height]];
176         displaySize.width *= ( ( CGFloat )fTitle->job->pixel_aspect_width ) / ( ( CGFloat )fTitle->job->pixel_aspect_height );   
177     }
178     else if (fTitle->job->pixel_ratio == 2) // Loose Anamorphic
179     {
180         display_width = output_width * output_par_width / output_par_height;
181         [fInfoField setStringValue:[NSString stringWithFormat:
182                                     @"Source: %dx%d, Output: %dx%d, Anamorphic: %dx%d",
183                                     fTitle->width, fTitle->height, output_width, output_height, display_width, output_height]];
184         
185         /* FIXME: use the original aspect ratio to calculate the displaySize, 
186             probably the size will not be the right one,
187             but at least the windows does not resize every time. */
188         displaySize.width *= ( ( CGFloat )fTitle->job->pixel_aspect_width) / ( ( CGFloat )fTitle->job->pixel_aspect_height );
189     }
190     else // No Anamorphic
191     {
192         [fInfoField setStringValue: [NSString stringWithFormat:
193                                      @"Source: %dx%d, Output: %dx%d", fTitle->width, fTitle->height,
194                                      fTitle->job->width, fTitle->job->height]];
195     }
196
197     NSSize viewSize = [self optimalViewSizeForImageSize:displaySize];
198     if( [self viewNeedsToResizeToSize:viewSize] )
199     {
200         [self resizeSheetForViewSize:viewSize];
201         [self setViewSize:viewSize];
202     }
203
204     // Show the scaled text (use the height to check since the width can vary
205     // with anamorphic video).
206     if( ( ( int )viewSize.height ) != fTitle->height )
207     {
208         CGFloat scale = viewSize.width / ( ( CGFloat ) fTitle->width );
209         NSString *scaleString = [NSString stringWithFormat:
210                                  NSLocalizedString( @" (Preview scaled to %.0f%% actual size)",
211                                                    @"String shown when a preview is scaled" ),
212                                  scale * 100.0];
213         [fInfoField setStringValue: [[fInfoField stringValue] stringByAppendingString:scaleString]];
214     }
215
216     [fPrevButton setEnabled: ( fPicture > 0 )];
217     [fNextButton setEnabled: ( fPicture < 9 )];
218 }
219
220 - (IBAction) deblockSliderChanged: (id) sender
221 {
222     if ([fDeblockSlider floatValue] == 4.0)
223     {
224     [fDeblockField setStringValue: [NSString stringWithFormat: @"Off"]];
225     }
226     else
227     {
228     [fDeblockField setStringValue: [NSString stringWithFormat: @"%.0f", [fDeblockSlider floatValue]]];
229     }
230         [self SettingsChanged: sender];
231 }
232
233 - (IBAction) SettingsChanged: (id) sender
234 {
235     hb_job_t * job = fTitle->job;
236     
237     autoCrop = ( [fCropMatrix selectedRow] == 0 );
238     [fCropTopStepper    setEnabled: !autoCrop];
239     [fCropBottomStepper setEnabled: !autoCrop];
240     [fCropLeftStepper   setEnabled: !autoCrop];
241     [fCropRightStepper  setEnabled: !autoCrop];
242
243     if( autoCrop )
244     {
245         memcpy( job->crop, fTitle->crop, 4 * sizeof( int ) );
246     }
247     else
248     {
249         job->crop[0] = [fCropTopStepper    intValue];
250         job->crop[1] = [fCropBottomStepper intValue];
251         job->crop[2] = [fCropLeftStepper   intValue];
252         job->crop[3] = [fCropRightStepper  intValue];
253     }
254     
255         if( [fAnamorphicPopUp indexOfSelectedItem] > 0 )
256         {
257         if ([fAnamorphicPopUp indexOfSelectedItem] == 2) // Loose anamorphic
258         {
259             job->pixel_ratio = 2;
260             [fWidthStepper setEnabled: YES];
261             [fWidthField setEnabled: YES];
262             /* We set job->width and call hb_set_anamorphic_size in libhb to do a "dry run" to get
263              * the values to be used by libhb for loose anamorphic
264              */
265             /* if the sender is the anamorphic popup, then we know that loose anamorphic has just
266              * been turned on, so snap the width to full width for the source.
267              */
268             if (sender == fAnamorphicPopUp)
269             {
270                 [fWidthStepper      setIntValue: fTitle->width-fTitle->job->crop[2]-fTitle->job->crop[3]];
271                 [fWidthField        setIntValue: fTitle->width-fTitle->job->crop[2]-fTitle->job->crop[3]];
272             }
273             job->width       = [fWidthStepper  intValue];
274             hb_set_anamorphic_size(job, &output_width, &output_height, &output_par_width, &output_par_height);
275             [fHeightStepper      setIntValue: output_height];
276             [fHeightField        setIntValue: output_height];
277             job->height      = [fHeightStepper intValue];
278             
279         }
280         else // must be "1" or strict anamorphic
281         {
282             [fWidthStepper      setIntValue: fTitle->width-fTitle->job->crop[2]-fTitle->job->crop[3]];
283             [fWidthField        setIntValue: fTitle->width-fTitle->job->crop[2]-fTitle->job->crop[3]];
284             
285             /* This will show correct anamorphic height values, but
286              show distorted preview picture ratio */
287             [fHeightStepper      setIntValue: fTitle->height-fTitle->job->crop[0]-fTitle->job->crop[1]];
288             [fHeightField        setIntValue: fTitle->height-fTitle->job->crop[0]-fTitle->job->crop[1]];
289             job->width       = [fWidthStepper  intValue];
290             job->height      = [fHeightStepper intValue];
291             
292             job->pixel_ratio = 1;
293             [fWidthStepper setEnabled: NO];
294             [fWidthField setEnabled: NO];
295         }
296         
297         /* if the sender is the Anamorphic checkbox, record the state
298          of KeepAspect Ratio so it can be reset if Anamorphic is unchecked again */
299         if (sender == fAnamorphicPopUp)
300         {
301             keepAspectRatioPreviousState = [fRatioCheck state];
302         }
303         [fRatioCheck setState:NSOffState];
304         [fRatioCheck setEnabled: NO];
305         
306         
307         [fHeightStepper setEnabled: NO];
308         [fHeightField setEnabled: NO];
309         
310     }
311     else
312         {
313         job->width       = [fWidthStepper  intValue];
314         job->height      = [fHeightStepper intValue];
315         job->pixel_ratio = 0;
316         [fWidthStepper setEnabled: YES];
317         [fWidthField setEnabled: YES];
318         [fHeightStepper setEnabled: YES];
319         [fHeightField setEnabled: YES];
320         [fRatioCheck setEnabled: YES];
321         /* if the sender is the Anamorphic checkbox, we return the
322          keep AR checkbox to its previous state */
323         if (sender == fAnamorphicPopUp)
324         {
325             [fRatioCheck setState:keepAspectRatioPreviousState];
326         }
327         
328         }
329         
330     job->keep_ratio  = ( [fRatioCheck state] == NSOnState );
331     
332         fPictureFilterSettings.deinterlace = [fDeinterlacePopUp indexOfSelectedItem];
333     /* if the gui deinterlace settings are fast through slowest, the job->deinterlace
334      value needs to be set to one, for the job as well as the previews showing deinterlacing
335      otherwise set job->deinterlace to 0 or "off" */
336     if (fPictureFilterSettings.deinterlace > 0)
337     {
338         job->deinterlace  = 1;
339     }
340     else
341     {
342         job->deinterlace  = 0;
343     }
344     fPictureFilterSettings.denoise     = [fDenoisePopUp indexOfSelectedItem];
345     
346     fPictureFilterSettings.detelecine  = [fDetelecineCheck state];
347     
348     if ([fDeblockField stringValue] == @"Off")
349     {
350     fPictureFilterSettings.deblock  = 0;
351     }
352     else
353     {
354     fPictureFilterSettings.deblock  = [fDeblockField intValue];
355     }
356     
357     fPictureFilterSettings.decomb = [fDecombPopUp indexOfSelectedItem];
358
359     if( job->keep_ratio )
360     {
361         if( sender == fWidthStepper || sender == fRatioCheck ||
362            sender == fCropTopStepper || sender == fCropBottomStepper )
363         {
364             hb_fix_aspect( job, HB_KEEP_WIDTH );
365             if( job->height > fTitle->height )
366             {
367                 job->height = fTitle->height;
368                 hb_fix_aspect( job, HB_KEEP_HEIGHT );
369             }
370         }
371         else
372         {
373             hb_fix_aspect( job, HB_KEEP_HEIGHT );
374             if( job->width > fTitle->width )
375             {
376                 job->width = fTitle->width;
377                 hb_fix_aspect( job, HB_KEEP_WIDTH );
378             }
379         }
380         // hb_get_preview can't handle sizes that are larger than the original title
381         // dimensions
382         if( job->width > fTitle->width )
383             job->width = fTitle->width;
384
385         if( job->height > fTitle->height )
386             job->height = fTitle->height;
387     }
388
389     [fWidthStepper      setIntValue: job->width];
390     [fWidthField        setIntValue: job->width];
391     if( [fAnamorphicPopUp indexOfSelectedItem] < 2 )
392         {
393         [fHeightStepper     setIntValue: job->height];
394         [fHeightField       setIntValue: job->height];
395     }
396     [fCropTopStepper    setIntValue: job->crop[0]];
397     [fCropTopField      setIntValue: job->crop[0]];
398     [fCropBottomStepper setIntValue: job->crop[1]];
399     [fCropBottomField   setIntValue: job->crop[1]];
400     [fCropLeftStepper   setIntValue: job->crop[2]];
401     [fCropLeftField     setIntValue: job->crop[2]];
402     [fCropRightStepper  setIntValue: job->crop[3]];
403     [fCropRightField    setIntValue: job->crop[3]];
404     /* Sanity Check Here for < 16 px preview to avoid
405      crashing hb_get_preview. In fact, just for kicks
406      lets getting previews at a min limit of 32, since
407      no human can see any meaningful detail below that */
408     if (job->width >= 64 && job->height >= 64)
409     {
410         // Purge the existing picture previews so they get recreated the next time
411         // they are needed.
412         [self purgeImageCache];
413         [self displayPreview];
414     }
415 }
416
417 - (IBAction) PreviousPicture: (id) sender
418 {   
419     if( fPicture <= 0 )
420     {
421         return;
422     }
423     fPicture--;
424     [self displayPreview];
425 }
426
427 - (IBAction) NextPicture: (id) sender
428 {
429     if( fPicture >= 9 )
430     {
431         return;
432     }
433     fPicture++;
434     [self displayPreview];
435 }
436
437 - (IBAction) ClosePanel: (id) sender
438 {
439     if ([delegate respondsToSelector:@selector(pictureSettingsDidChange)])
440         [delegate pictureSettingsDidChange];
441
442     [NSApp endSheet:[self window]];
443     [[self window] orderOut:self];
444 }
445
446 - (BOOL) autoCrop
447 {
448     return autoCrop;
449 }
450 - (void) setAutoCrop: (BOOL) setting
451 {
452     autoCrop = setting;
453 }
454
455 - (BOOL) allowLooseAnamorphic
456 {
457     return allowLooseAnamorphic;
458 }
459
460 - (void) setAllowLooseAnamorphic: (BOOL) setting
461 {
462     allowLooseAnamorphic = setting;
463 }
464
465 - (int) detelecine
466 {
467     return fPictureFilterSettings.detelecine;
468 }
469
470 - (void) setDetelecine: (int) setting
471 {
472     fPictureFilterSettings.detelecine = setting;
473 }
474
475 - (int) deinterlace
476 {
477     return fPictureFilterSettings.deinterlace;
478 }
479
480 - (void) setDeinterlace: (int) setting {
481     fPictureFilterSettings.deinterlace = setting;
482 }
483 - (int) decomb
484 {
485     return fPictureFilterSettings.decomb;
486 }
487
488 - (void) setDecomb: (int) setting {
489     fPictureFilterSettings.decomb = setting;
490 }
491 - (int) denoise
492 {
493     return fPictureFilterSettings.denoise;
494 }
495
496 - (void) setDenoise: (int) setting
497 {
498     fPictureFilterSettings.denoise = setting;
499 }
500
501 - (int) deblock
502 {
503     return fPictureFilterSettings.deblock;
504 }
505
506 - (void) setDeblock: (int) setting
507 {
508     fPictureFilterSettings.deblock = setting;
509 }
510
511 - (void)showPanelInWindow: (NSWindow *)fWindow forTitle: (hb_title_t *)title
512 {
513     [self SetTitle:title];
514
515     [NSApp beginSheet:[self window]
516        modalForWindow:fWindow
517         modalDelegate:nil
518        didEndSelector:nil
519           contextInfo:NULL];
520 }
521
522
523 // This function converts an image created by libhb (specified via pictureIndex) into
524 // an NSImage suitable for the GUI code to use. If removeBorders is YES,
525 // makeImageForPicture crops the image generated by libhb stripping off the gray
526 // border around the content. This is the low-level method that generates the image.
527 // -imageForPicture calls this function whenever it can't find an image in its cache.
528 + (NSImage *) makeImageForPicture: (int)pictureIndex
529                 libhb:(hb_handle_t*)handle
530                 title:(hb_title_t*)title
531                 removeBorders:(BOOL)removeBorders
532 {
533     if (removeBorders)
534     {
535         //     |<---------- title->width ----------->|
536         //     |   |<---- title->job->width ---->|   |
537         //     |   |                             |   |
538         //     .......................................
539         //     ....+-----------------------------+....
540         //     ....|                             |....<-- gray border
541         //     ....|                             |....
542         //     ....|                             |....
543         //     ....|                             |<------- image
544         //     ....|                             |....
545         //     ....|                             |....
546         //     ....|                             |....
547         //     ....|                             |....
548         //     ....|                             |....
549         //     ....+-----------------------------+....
550         //     .......................................
551
552         static uint8_t * buffer;
553         static int bufferSize;
554
555         // Make sure we have a big enough buffer to receive the image from libhb. libhb
556         // creates images with a one-pixel border around the original content. Hence we
557         // add 2 pixels horizontally and vertically to the buffer size.
558         int srcWidth = title->width + 2;
559         int srcHeight= title->height + 2;
560         int newSize;
561         newSize = srcWidth * srcHeight * 4;
562         if( bufferSize < newSize )
563         {
564             bufferSize = newSize;
565             buffer     = (uint8_t *) realloc( buffer, bufferSize );
566         }
567
568         hb_get_preview( handle, title, pictureIndex, buffer );
569
570         // Create an NSBitmapImageRep and copy the libhb image into it, converting it from
571         // libhb's format to one suitable for NSImage. Along the way, we'll strip off the
572         // border around libhb's image.
573         
574         // The image data returned by hb_get_preview is 4 bytes per pixel, BGRA format.
575         // Alpha is ignored.
576         
577         int dstWidth = title->job->width;
578         int dstHeight = title->job->height;
579         NSBitmapFormat bitmapFormat = (NSBitmapFormat)NSAlphaFirstBitmapFormat;
580         NSBitmapImageRep * imgrep = [[[NSBitmapImageRep alloc]
581                 initWithBitmapDataPlanes:nil
582                 pixelsWide:dstWidth
583                 pixelsHigh:dstHeight
584                 bitsPerSample:8
585                 samplesPerPixel:3   // ignore alpha
586                 hasAlpha:NO
587                 isPlanar:NO
588                 colorSpaceName:NSCalibratedRGBColorSpace
589                 bitmapFormat:bitmapFormat
590                 bytesPerRow:dstWidth * 4
591                 bitsPerPixel:32] autorelease];
592
593         int borderTop = (srcHeight - dstHeight) / 2;
594         int borderLeft = (srcWidth - dstWidth) / 2;
595         
596         UInt32 * src = (UInt32 *)buffer;
597         UInt32 * dst = (UInt32 *)[imgrep bitmapData];
598         src += borderTop * srcWidth;    // skip top rows in src to get to first row of dst
599         src += borderLeft;              // skip left pixels in src to get to first pixel of dst
600         for (int r = 0; r < dstHeight; r++)
601         {
602             for (int c = 0; c < dstWidth; c++)
603 #if TARGET_RT_LITTLE_ENDIAN
604                 *dst++ = Endian32_Swap(*src++);
605 #else
606                 *dst++ = *src++;
607 #endif
608             src += (srcWidth - dstWidth);   // skip to next row in src
609         }
610
611         NSImage * img = [[[NSImage alloc] initWithSize: NSMakeSize(dstWidth, dstHeight)] autorelease];
612         [img addRepresentation:imgrep];
613
614         return img;
615     }
616     else
617     {
618         // Make sure we have big enough buffer
619         static uint8_t * buffer;
620         static int bufferSize;
621
622         int newSize;
623         newSize = ( title->width + 2 ) * (title->height + 2 ) * 4;
624         if( bufferSize < newSize )
625         {
626             bufferSize = newSize;
627             buffer     = (uint8_t *) realloc( buffer, bufferSize );
628         }
629
630         hb_get_preview( handle, title, pictureIndex, buffer );
631
632         // The image data returned by hb_get_preview is 4 bytes per pixel, BGRA format.
633         // We'll copy that into an NSImage swapping it to ARGB in the process. Alpha is
634         // ignored.
635         int width = title->width + 2;      // hblib adds a one-pixel border to the image
636         int height = title->height + 2;
637         int numPixels = width * height;
638         NSBitmapFormat bitmapFormat = (NSBitmapFormat)NSAlphaFirstBitmapFormat;
639         NSBitmapImageRep * imgrep = [[[NSBitmapImageRep alloc]
640                 initWithBitmapDataPlanes:nil
641                 pixelsWide:width
642                 pixelsHigh:height
643                 bitsPerSample:8
644                 samplesPerPixel:3   // ignore alpha
645                 hasAlpha:NO
646                 isPlanar:NO
647                 colorSpaceName:NSCalibratedRGBColorSpace
648                 bitmapFormat:bitmapFormat
649                 bytesPerRow:width * 4
650                 bitsPerPixel:32] autorelease];
651
652         UInt32 * src = (UInt32 *)buffer;
653         UInt32 * dst = (UInt32 *)[imgrep bitmapData];
654         for (int i = 0; i < numPixels; i++)
655 #if TARGET_RT_LITTLE_ENDIAN
656             *dst++ = Endian32_Swap(*src++);
657 #else
658             *dst++ = *src++;
659 #endif
660
661         NSImage * img = [[[NSImage alloc] initWithSize: NSMakeSize(width, height)] autorelease];
662         [img addRepresentation:imgrep];
663
664         return img;
665     }
666 }
667
668 // Returns the preview image for the specified index, retrieving it from its internal
669 // cache or by calling makeImageForPicture if it is not cached. Generally, you should
670 // use imageForPicture so that images are cached. Calling makeImageForPicture will
671 // always generate a new copy of the image.
672 - (NSImage *) imageForPicture: (int) pictureIndex
673 {
674     // The preview for the specified index may not currently exist, so this method
675     // generates it if necessary.
676     NSString * key = [NSString stringWithFormat:@"%d", pictureIndex];
677     NSImage * theImage = [fPicturePreviews objectForKey:key];
678     if (!theImage)
679     {
680         theImage = [PictureController makeImageForPicture:pictureIndex libhb:fHandle title:fTitle removeBorders: NO];
681         [fPicturePreviews setObject:theImage forKey:key];
682     }
683     return theImage;
684 }
685
686 // Purges all images from the cache. The next call to imageForPicture will cause a new
687 // image to be generated.
688 - (void) purgeImageCache
689 {
690     [fPicturePreviews removeAllObjects];
691 }
692
693 @end
694
695 @implementation PictureController (Private)
696
697 //
698 // -[PictureController(Private) optimalViewSizeForImageSize:]
699 //
700 // Given the size of the preview image to be shown, returns the best possible
701 // size for the view.
702 //
703 - (NSSize)optimalViewSizeForImageSize: (NSSize)imageSize
704 {
705     // The min size is 320x240
706     CGFloat minWidth = 320.0;
707     CGFloat minHeight = 240.0;
708
709     // The max size of the view is when the sheet is taking up 85% of the screen.
710     NSSize screenSize = [[NSScreen mainScreen] frame].size;
711     NSSize sheetSize = [[self window] frame].size;
712     NSSize viewAreaSize = [fPictureViewArea frame].size;
713     CGFloat paddingX = sheetSize.width - viewAreaSize.width;
714     CGFloat paddingY = sheetSize.height - viewAreaSize.height;
715     CGFloat maxWidth = (0.85 * screenSize.width) - paddingX;
716     CGFloat maxHeight = (0.85 * screenSize.height) - paddingY;
717     
718     NSSize resultSize = imageSize;
719     
720     // Its better to have a view that's too small than a view that's too big, so
721     // apply the maximum constraints last.
722     if( resultSize.width < minWidth )
723     {
724         resultSize.height *= (minWidth / resultSize.width);
725         resultSize.width = minWidth;
726     }
727     if( resultSize.height < minHeight )
728     {
729         resultSize.width *= (minHeight / resultSize.height);
730         resultSize.height = minHeight;
731     }
732     if( resultSize.width > maxWidth )
733     {
734         resultSize.height *= (maxWidth / resultSize.width);
735         resultSize.width = maxWidth;
736     }
737     if( resultSize.height > maxHeight )
738     {
739         resultSize.width *= (maxHeight / resultSize.height);
740         resultSize.height = maxHeight;
741     }
742     
743     return resultSize;
744 }
745
746 //
747 // -[PictureController(Private) resizePanelForViewSize:animate:]
748 //
749 // Resizes the entire sheet to accomodate a view of a particular size.
750 //
751 - (void)resizeSheetForViewSize: (NSSize)viewSize
752 {
753     // Figure out the deltas for the new frame area
754     NSSize currentSize = [fPictureViewArea frame].size;
755     CGFloat deltaX = viewSize.width - currentSize.width;
756     CGFloat deltaY = viewSize.height - currentSize.height;
757
758     // Now resize the whole panel by those same deltas, but don't exceed the min
759     NSRect frame = [[self window] frame];
760     NSSize maxSize = [[self window] maxSize];
761     NSSize minSize = [[self window] minSize];
762     frame.size.width += deltaX;
763     frame.size.height += deltaY;
764     if( frame.size.width < minSize.width )
765     {
766         frame.size.width = minSize.width;
767     }
768     if( frame.size.height < minSize.height )
769     {
770         frame.size.height = minSize.height;
771     }
772
773     // But now the sheet is off-center, so also shift the origin to center it and
774     // keep the top aligned.
775     if( frame.size.width != [[self window] frame].size.width )
776         frame.origin.x -= (deltaX / 2.0);
777
778     if( frame.size.height != [[self window] frame].size.height )
779         frame.origin.y -= deltaY;
780
781     [[self window] setFrame:frame display:YES animate:YES];
782 }
783
784 //
785 // -[PictureController(Private) setViewSize:]
786 //
787 // Changes the view's size and centers it vertically inside of its area.
788 // Assumes resizeSheetForViewSize: has already been called.
789 //
790 - (void)setViewSize: (NSSize)viewSize
791 {
792     [fPictureView setFrameSize:viewSize];
793     
794     // center it vertically
795     NSPoint origin = [fPictureViewArea frame].origin;
796     origin.y += ([fPictureViewArea frame].size.height -
797                  [fPictureView frame].size.height) / 2.0;
798     [fPictureView setFrameOrigin:origin];
799 }
800
801 //
802 // -[PictureController(Private) viewNeedsToResizeToSize:]
803 //
804 // Returns YES if the view will need to resize to match the given size.
805 //
806 - (BOOL)viewNeedsToResizeToSize: (NSSize)newSize
807 {
808     NSSize viewSize = [fPictureView frame].size;
809     return (newSize.width != viewSize.width || newSize.height != viewSize.height);
810 }
811
812 @end