Notes and Change History

Copyright (c) 2002-2003 Marco Piovanelli

• 20020722: Met up with Marco Silvestri in Egna/Neumarkt.

• 20020723: I need to get up to speed on digital image processing, at least at a beginner
  level.  Topics in need of a rehash: color spaces (RGB, HSL, HSV, YCrCb, XYZ, etc.),
  conversions between color spaces, gamma correction, convolutions, Gaussian blurs,
  sharpen and unsharp filters, luminance mapping, bitmap rotation.

  Here's what looks like a good textbook on the subject: "Digital Image Processing" (2nd ed)
  by Rafael C. Gonzalez and Richard E. Woods, published by Addison-Wesley.

  I'm revising and extending the old MLEffect class hierarchy.
  For instance, I'm adding an ML3x3Convolution effect that applies a 3x3 convolution
  kernel to a pixmap (an MLOffscreenCanvas in framework jargon).  This allows me to
  perform simple filters like blur, sharpen and edge detection.  I'm pleasantly
  surprised with the speed of these filters: they all run in a split second on
  largish (around 1 megapixel), full color (32-bit deep) pixmaps, on a lowly 350 MHz
  G4 processor.  Sure, I've turned on all compiler optimizations on the inner loop,
  but still, the code isn't terribly hand-optimized.  I've found some slides by
  Wojciech Jarosz (proceedings of ACM Siggraph?) which suggest some radical algorithmic
  optimizations to the basic "sliding window" approach to convolutions, but I think
  I'll ignore these suggestions for the time being.

• 20020806: Revised specs from Marco.

  Implemented the MLAutoLevel class according to the new specs.  This simple, two-pass
  filter works like this: for each color channel (R, G, B), it finds the minimum and maximum
  sample, then it replaces each sample with the normalized value mapped to the [0, 255] interval.

                  ( oldvalue - min )
     newvalue =   ------------------ * 255
                     ( max - min )

  Care should be taken in the fringe case where ( min == max ).

  Implemented a new MLRGBTransform filter which operates on single pixels independently
  like this:
        r'        x11  x12  x13       r         y1
      [ g' ] = [  x21  x22  x23 ] * [ g ]  +  [ y2 ]
        b'        x31  x32  x33       b         y3

  A lot of simple filters can be expressed in terms of the above equation, for suitable
  values of the X matrix and the Y offset vector.  For example, luminance mapping is
  represented by the following matrix:
        .222  .707  .071
      [ .222  .707  .071 ]
        .222  .707  .071

• 20020807: Working on image importing.

  Implemented MLMacEvent, MLMacEventHandler classes and MLMacEventHandlerT template class.
  These are useful wrappers around Carbon events and Carbon event handlers.
  The MLMacEventHandlerT template allows easy routing of a Carbon event to a member function,
  along the lines of PowerPlant's TEventHandler template.

  The application objects registers a kEventVolumeMounted handler with the system, so it gets
  notified as soon as a new volume is mounted.  The VolumeMounted() method inspects the
  filesystemID of the volume.  It is my understanding that this field can be used to tell
  SmartMedia / Compact Flash cards from other removable volumes of comparable capacity.
  For example, the SmartMedia card I'm using for testing purposes gets mounted as a volume
  with a filesystemID of 18771 ('IS') by the Delkin eFilm driver I'm using on OS 9.2.2.
  Anyway, the debug build of DE prints the filesystemIDs of all mounted volumes to the
  debug console.

  If DE decides the mounted volume is a photographic medium, it spawns an importer thread
  that scans the whole volume and copies all the image files it can find to the hard disk.
  This is a cooperative, rather than a preemptive, thread, for two reasons: 1. the C++ framework
  I'm using already features a handy wrapper class around cooperative threads, and 2. most of
  the Carbon API is still not reentrant, even on OS X, so it can't be used safely from
  preemptive threads.  In the (OS X-only) future, we should consider migrating to a pthread.

• 20020808: I registered with Kodak Developer Relations (username: "piovanel") to gain
  access to the Kodak Professional 8660 Thermal Printer.

• 20020813: Added various new classes: DigitalEmotionPhoto, DigitalEmotionKeywordFamily,
  MLDate.  A DigitalEmotionPhoto is a convenient wrapper around an MLOffscreenCanvas
  which belongs to a DigitalEmotionAlbum and maintains some additional metadata.
  A DigitalEmotionKeywordFamily is basically a named set of keywords.
  Each DigitalEmotionAlbum maintains an array of keyword families.

• 20020909: New classes for writing Mac OS X-style property lists as XML files.
  This will be used to save state information for albums and photos.
  Incorporated expat (James Clark's SAX-style XML parser) and various XML-related
  classes into the project.  Implemented an MLPropertyListParser class that reads
  .plist files using expat.
  Album state is now saved to disk between sessions in a "manifest.plist" file
  in the .album folder.  Whew!

• 20020910: DE's specs call for 1, 4, 9 or 16 photos visible at the same time in
  the viewer area.  The naive approach followed so far just loads the photo files
  (JPEG or TIFF) into corresponding MLPictureView objects.  It's obvious that this
  can consume an awful lot of memory.  Even a 1024x768 pixmap (which is considered
  poor resolution by today's standards of digital photography) takes up 3 MB in
  memory (remember: all images are expanded to 32-bit depth for ease of manipulation),
  which means 48 MB would have to be allocated just for the expanded pixmaps to
  fill up the 4x4 viewer.  To lower these requirements, we now create JPEG thumbnails
  of all photo files right after importing them from external media.  These thumbnails
  are completely contained within a 320x320 rectangle, and maintain the original
  aspect ratio.  Each thumbnail takes up no more than 400K when expanded in memory.
  The thumbnail are stored in a "thumbnail" folder within the .album folder.
  They have the same names as the corresponding photo files, except for the extension,
  which is always ".jpg", regardless of the format of the original file.

• 20020918: Registered Digital Emotion's creator type ("Jóga" = 0x4A976761) with Apple.
  Repackaged application as an .app bundle.
  Added application icons, courtesy of IconFactory (
  Added properly formed Info.plist file; embryonic English and Italian .strings files.

• 20020918: Got a phone call from a Sony rep today.  He asked a few questions about
  this project.  Hopefully they'll help me find a way to talk to the Sony printers
  via SCSI on Mac OS X.  AFAIK, the official Sony libraries for this only work on
  Mac OS Classic.

• 20020918: Added the DigitalEmotionPhotoRing class, a straightforward subclass of
  MLAbsButtonFeedback that gives me simple colored rings around pictures.
  I love the flexibility of framework buttons.

• 20020918: Re-thought the layout of the Digital Emotion folder hierarchy.
  This is a hierarchy of files and folders rooted in the "Digital Emotion" directory
  within the Documents directory of the current user.  It looks like this:
      Digital Emotion  ---+---  Albums
                          +---  Common Keywords
                          +---  Frames
                          +---  Icons

  The Albums folder is the preferred location for albums, which are themselves
  packaged folders (directories that look like single items in the Mac OS X Finder)
  with the following structure:
      example.album    ---+---  photo
                          +---  thumbnail
                          +---  manifest.plist

  Where "photo" is a directory where pictures from the camera are stored, never to
  be altered; "thumbnail" is a parallel directory holding scaled-down, possibly
  rotated and/or retouched versions of the original pictures.  Finally, "manifest.plist"
  is an XML file (conforming to Apple's Property List DTD, version 0.9) which stores
  just about everything else needed to make the album persistent across sessions,
  including per-album keywords.

• 20020920: Added tentative support for 2nd screen.  Can't test this code here as
  none of my macs support dual monitors.

• 20020926: Got Sony printer (a UP-D70A unit) from Marco.

• 20020926: Implemented MLGraphicsImporter and MLQTMatrix classes.
  MLGraphicsImporter is a wrapper around a QuickTime GraphicsImportComponent, much in
  the same way as MLGraphicsExporter is a wrapper around GraphicsExportComponent.
  MLQTMatrix is a wrapper around a QuickTime MatrixRecord, which is used to describe
  affine transforms.
  Rewritten portions of MLOffscreenCanvas to take advantage of MLGraphicsImporter.

• 20020927: Implemented MLQTUserData class, a wrapper around the QuickTime user data list
  abstraction.  This allows me to extract image meta data collected by QuickTime graphics
  importers, including EXIF tags embedded in TIFF and JPEG files.

• 20020928: Got a confirmation message from Apple stating that my creator registation
  request was successfully processed.  Unfortunately, they registered the wrong creator
  code: 0x4AF36761 instead of 0x4A976761, as I requested.  This is obviously due to some
  kind of text encoding problem.  Fortunately, I dig text encodings, and I think I uncovered
  at least three text encoding-related bugs in Apple's creator registration web and mail
  interface.  I reported the problems to Apple and requested a correction.

• 20020928: Unpacking, connecting and testing the Sony UP-D70A printer.

  Wrote some glue code to access the "UPLib.PPC" library (an InterfaceLib-based
  shared library for direct SCSI access to the printer, from the Sony SDK) from
  a Carbon application.  Of course, this glue only runs on classic Mac OS.
  I still have to figure out a way to drive multiple Sony printers at once
  under OS X.  Recompiling the UPLib library as a Carbon target would be the
  best approach, but I don't have the source code and I doubt Sony would grant
  me access to it.  Other possible routes I can think about include: 1. trick
  the existing library into linking to a fake InterfaceLib on OS X; and
  2. reverse-engineer the library, figure out what kind of SCSI commands it
  sends the printer, and emulate the relevant commands with OS X-compatible code.

• 20020930: Apple acknowledged and corrected the creator registration problem reported
  a couple of days ago.

• 20020930: Implemented the "Import" button.
  Extended the MLAbstractButton class so that it detects double-clicks and invokes
  an optional mDoubleClickAction member.

• 20021001: Major breakthrough: communication with the Sony printer is now
  possible in Mac OS X, thanks to some serious CFM voodoo.
  Started designing and implementing the DigitalEmotionPrintJob class, which coordinates
  the process of transferring a pixmap (modeled by an MLOffscreenCanvas object) to a
  printer.  Here's the basic idea: the process involves creating a spool file holding
  the data ready to be sent to the printer (typically, a 24-bit deep pixmap in chunky
  or planar format).  Print job objects will then be held in a priority queue.  As soon
  as one of the connected printers is ready, a job is dequeued (unspooled).

• 20021002: First printing tests.

  Memory footprint considerations: the largest printable area for my test unit is a
  whooping 3508 x 2550 pixels (8.5 megapixels), or about 29.7 x 21.6 cm (very close to
  the standard A4 paper size) at the standard resolution of 300 dots per inch.
  That means over 25 MB per job need to be transferred, if we want to take advantage
  of the whole sheet.
  The size of a typical test photo is 2304 x 1536 (about 3.4 megapixels) which takes
  up 13.5 MB in RAM as a standard 32-bit deep pixmap.  A naive approach to scaling
  the photo so that it fits the sheet (using a scale factor of, say, 1.5) involves
  CopyBits'ing the 2304 x 1536 pixmap onto a 3456 x 2304 pixmap, then spooling the
  scaled pixmap.  At CopyBits time, 13.5 * (1 + 1.5^2) MB = 43.875 MB of RAM are taken
  up by the pixmaps alone.  That sounds like a hell of a lot of memory.  Gotta think
  of a more memory-efficient way to scale the pixmap.

• 20021009: Got a phone call from a Sony rep in Rome who gave me the following pointers:

	Sony UP-D70A driver page:			<>
	Sony technical support person:		Ruggero Pezzolo (+39.02618381)

• 20021010: Implemented DigitalEmotionPanner class.  This is a transparent view meant
  to be superimposed to the single-photo viewer.  It tracks mouse movements and broadcasts
  kCommandPan messages accordingly.  The application listens to such messages and alters
  the transform matrix of the selected photo to reflect the new pan delta.

• 20021014: My attempts to use graphics importers in conjunction with QuickTime transforms
  to perform all rotate/scale/pan operations on canvases were not completely successful, so
  I decided to scrap a bunch of code and start from scratch with my own code, which
  operates directly on pixel surfaces.  A new MLAffineMatrix class represents a 3x3 affine
  transform of the bidimensional plane, and allows several operations on affine matrices,
  including product (composition), inversion, translation, rotation and scale.
  MLAffineMatrix::TransformPoint applies the transform to a point in floating-point
  coordinates (MLFloatPoint).  A new subclass of MLEffect, MLAffineTransform, applies
  an MLAffineMatrix to an MLOffscreenCanvas.

  TODO: MLAffineTransform currently uses a very simple linear sampling algorithm, which
  is fast, but generates artifacts (jaggies), degrading the image quality.  This is kind
  of OK when displaying the image on screen, but probably not when printing.  We should
  implement alternate, slower but more accurate, algorithms to be used when creating print
  Major revision to the MLMediaCache class, for additional flexibility.  In particular,
  it is now possible to manually add cacheables (instances of MLCacheable) to the cache,
  without having to go through MLMediaCache::Acquire().  Also, specific cacheable objects
  can be purged from the media cache.

• 20021015: Factored DigitalEmotionScanner out of DigitalEmotionImporter.
  DigitalEmotionScanner is a generic facility for asynchronous scanning of file system trees.
  New DigitalEmotionAuxFolderScanner class enumerates available frames and icons and creates
  thumbnails as necessary.
  A new MLAlphaBlend subclass of MLTransition combines two canvases using the alpha channel
  from one of them.

  Added logic for displaying, scrolling and applying frames.

• 20021019: Modified color cursors with sparse color tables with less than 2^depth entries
  to work around Radar #2997797 (garbled colors and random crashes in Jaguar).

• 20021020: Moved DigitalEmotionPrintJob to a separate source file (DigitalEmotionPrintJob.cpp).
  Spooling and unspooling operations now use a fixed 64K buffer rather than using an arbitrary
  chunk of memory.

  Print job objects are now saved in the album manifest as part of an album's persistent state.

• 20021021: Performed several SCSI tests with Mac OS X 10.1.5 and Mac OS X 10.2.1, using both
  my Adaptec 2930CU PCI card and the Ratoc Systems FR1SX FireWire to UltraSCSI Converter.
  The Apple System Profiler can see the Sony printer in all cases.  On the other hand, Digital
  Emotion doesn't seem to see the printer using the FireWire converter, but it does see it
  with the Adaptec card.  The bad news is that when I actually try to print, the InitPrint
  command always fails with a -7918 error (SCSI data overrun/underrun) on Mac OS X, while it
  worked, under identical conditions, on Mac OS 9.  I'm afraid this means Digital Emotion will
  have to be used under Mac OS 9 until I or Sony find a workaround.  :-(

  DigitalEmotionAlbum::SpoolPhoto() now magnifies the work canvas by blitting it into a larger
  "print" canvas which uses up all the available printable area.  This takes up an awful lot
  of memory so it can fail if you have many applications open.  Also, the aspect ratio may not
  be maintained exactly (TODO: fix this).

  DigitalEmotionPrintJob::CreateSpoolFile() can now flip the orientation (landscape <-> portrait)
  of an image on the fly while spooling a pixmap to disk.  This is necessary for potrait-oriented
  pictures as every image has to ultimately be sent to the Sony printer in landscape mode, to make
  the best possible use of the printable area.

  A 52-byte header is now prepended to the spool file (which contains raw, uncompressed 24-bit
  RGB data ready to be sent to the printer with no additional modifications) so the file looks
  like a .ppm file (.ppm is the Portable Pixmap File Format created by Jef Poskanzer) and can
  be opened by utilities such as Thorsten Lemke's excellent GraphicConverter.

  Bug fix: the Next button will no longer let you increment the current page past the last photo.

• 20021022: A new DigitalEmotionPrintJobManager class coordinates the unspooling of print jobs
  for an album.  This is a subclass of MLMacThread so it does most of the work in its own thread.
  It looks for printers on the SCSI bus, polls them to find the available ones, dequeues jobs from
  the album's print queue, sends spool files and starts printing.

  Packaged up the current build (in both its cfm.debug and incarnations) and posted it to:
  along with these notes for Marco to check out.

• 20021023: Whoops.  A critical line was commented out in yesterday's build, stopping print jobs
  from being completed.  I'm posting another build which corrects this problem:

• 20021104: Special bar now wraps around.

• 20021106: Preferences.  Digital Emotion now maintains a preferences file, in .plist (XML) format,
  containing various global settings.  Defined keys so far are "rotate_step" and "zoom_step".

• 20021106: This is not in the specs, but I figured it's a useful feature anyway, and it's pretty
  cool, too.  Holding down the option key while dragging the image in the single-photo viewer
  *rotates*, rather than pans, the image around its center.

• 20021106: Wrote a quick handbook (in Italian) for Button Tool and sent it to Marco.

• 20021106: The size of the media cache can now be specified in the preferences file.  The relevant
  key is "media_cache_size", the value is expressed in megabytes, the default value is 16.

• 20021107: Color adjustment settings for the Sony printer are now stored persistently in the
  preferences file in their own dictionary, under the "printer_settings_Sony-UPD70A" key.
  When I add support for more printer models in the future, each model will get its private
  key and dictionary.

• 20021107: Corrected error in MLUniString::StringToInt() that caused representations of negative
  integers to be parsed incorrectly.

• 20021107: In the debug build, color adjustment settings for all connected Sony printers are now
  dumped to the debug console.  These settings seem to be retained by the printer in some kind of
  non-volatile memory.

• TODO: Find a way to parse PhotoShop curves (.acv) files.  I haven't been able to find a format spec
  on the web, but from a quick glance, it looks like the file contains sequences of points (as <y,x>
  coordinate pairs) which must be interpolated to generate the curves.  I don't know what kind of
  interpolation algorithm PhotoShop uses, probably natural cubic splines.

• 20021110: Added MLGraphicsExporter::Progress class, a simple wrapper around a QuickTime
  ICMProgressProcRecord structure.  Added MLGraphicsExporter::SetProgress() method to attach
  a Progress object to a graphics exporter.  The exporter will supposedly call Progress::OpenProgress(),
  Progress::DoProgress and Progress::CloseProgress() during potentially lengthy operations such
  as JPEG compression.  Digital Emotion uses a simple subclass of MLGraphicsExporter::Progress
  named DigitalEmotionYieldingProgress which just yields the cpu to other threads once in a while
  from DoProgress().  Problem is, the JPEG exporter never seems to invoke this callback.

• 20021112: Persistent selections.  The selected photos in an album are marked by a red frame.
  The red frame is now shown in the one-photo viewer as well, as per Marco's request.
  The orientation buttons (rotate 90 clockwise and rotate 90 counterclockwise) can now act on multiple
  selections.  Implemented the (De-)Select All button.

• 20021112: Another major revision to the MLSharable/MLCacheable/MLMediaCache classes.  The media
  cache now maintains just one map object (previously, it would use two: a "cache" map for "active"
  objects and a "garbage" map for "unused" objects).  This simplifies the design without impacting
  on the flexibility.  The MLSharable::NoMoreReferences() method has been eliminated -- all sharables
  are now immediately deleted when their reference count drops to zero.  Consequently, the
  MLMediaCache::ObjectReleased() method has been eliminated as well.  The media cache now uses
  standard Acquire/Release semantics for cached objects, just like everyone else (previously,
  the media cache maintained privileged references that were not reference-counted).
  Finally, MLMediaCache::Purge() can now be used to detach cacheables off the media cache, without
  necessarily deleting them.  This is useful in Digital Emotion when cached thumbnail files are

• 20021112: Working on palette windows: there will be two floating palettes:

    1. the Color Palette, with sliders for brightness/contrast adjustments, etc., shown when the Adjust
       button is clicked, and

    2. the Text Palette, with an edit field for entering text and related controls, shown when the Text
       button is clicked.

• 20021112: First stab at contrast control.

• 20021113: Working on constrast/brightness controls.

  This is the basic idea: contrast is a real nonnegative number representing the slope of the
  LUT (lookup table) curve (which is actually a straight line as far as these simple controls
  are concerned), while brightness is another real number, in the range [-1, +1], used to offset
  the LUT curve up or down.  The default contrast is 1.0 and the default brightness is 0.0 --
  the corresponding LUT curve is a graph of the equation y = x.  The value of the contrast slider
  is a linear function of the angle formed by the LUT line with the x axis or, put another way,
  of the arctangent of the contrast.

  Given a contrast/brightness pair, and a tone value in the range [0, 1], the remapped value
  is calculated according to this formula:
      tone := clamp ( tone * contrast + ( 1.0 - contrast ) / 2.0 + brightness, 0.0, 1.0 )

  Note how the LUT line always intersects the [0.5, 0.5] point when brightness = 0.

  The specs also call for saturation and unsharp controls.  Here's an interesting article suggesting
  a technique to control multiple parameters at once using extrapolation:

  See also this article from the same series, about using 4x4 matrices for representing various RGB
  manipulations (this is very much like the idea that gave birth to my MLRGBTransform class, see entry
  20020806 in this log):

  Added AUTO and RESET buttons to the color palette.  The AUTO button applies an MLAutoLevel
  filter to the image, resets the contrast/brightness values and disables the contrast/brightness
  sliders.  The RESET button resets all color controls and reverts the image to its original state.
  I'm not happy with the MLAutoLevel filter.  In most test cases, it does absolutely nothing.
  I should look into a more sophisticated filter, like Gimp Equalize.

• 20021114: New build posted to <>.

  Added DigitalEmotionToneCurveViewer class.  This is a simple leaf view that displays a LUT curve.
  Added an instance of DigitalEmotionToneCurveViewer to the color palette, to show how the LUT
  curve changes as the contrast and brightness sliders are moved.

  Experimental MLUseCanvas optimization: we now keep track of the current gworld in a class variable,
  and do nothing if the canvas passed to the costructor is the same as the current gworld.
  This cuts down dramatically on the number of unneeded calls to GetGWorld(), SetGWorld(), LockPixels()
  and UnlockPixels(), which will hopefully speed up execution, especially in tight loops calling
  MLCanvas members.  OTOH, this change makes MLUseCanvas thread-unsafe, and may be dangerous if
  someone changes the port behind our backs and doesn't put it back.

• 20021117: Recompiled targets with the recently released CodeWarrior Pro 8.3.

• 20021118: Optimized MLAffineTransform::EffectLoop32Bit().  The inner loop, which applies the
  inverse transform to a row of pixels in the destination pixmap, is now just 27 instructions long.

		000000F0: EC2B193A  fmadds     fp1,fp11,fp4,fp3        ; fp1 := fp11 * fp4 + fp3 -- multiply/add combined!
		000000F4: 7D234B78  mr         r3,r9
		000000F8: EC08113A  fmadds     fp0,fp8,fp4,fp2
		000000FC: FC20081E  fctiwz     fp1,fp1
		00000100: FC00001E  fctiwz     fp0,fp0
		00000104: D821FFE0  stfd       fp1,-32(SP)             ; CodeWarrior generates a store/load pair to convert
		00000108: 83A1FFE4  lwz        r29,-28(SP)             ; a float to an int -- too bad there isn't a better way
		0000010C: D801FFE8  stfd       fp0,-24(SP)
		00000110: 2C1D0000  cmpwi      r29,$0000
		00000114: 8181FFEC  lwz        r12,-20(SP)
		00000118: 41800030  blt        *+48                    ; $00000148
		0000011C: 7C1D5800  cmpw       r29,r11
		00000120: 40800028  bge        *+40                    ; $00000148
		00000124: 2C0C0000  cmpwi      r12,$0000
		00000128: 41800020  blt        *+32                    ; $00000148
		0000012C: 7C0C5000  cmpw       r12,r10
		00000130: 40800018  bge        *+24                    ; $00000148
		00000134: 7C6C01D6  mullw      r3,r12,r0
		00000138: 57AC103A  slwi       r12,r29,2
		0000013C: 5463103A  slwi       r3,r3,2
		00000140: 7C651A14  add        r3,r5,r3
		00000144: 7C6C182E  lwzx       r3,r12,r3
		00000148: C007000C  lfs        fp0,12(r7)              ; why this load? this should use a register.
		0000014C: 907F0000  stw        r3,0(r31)
		00000150: 3BFF0004  addi       r31,r31,4
		00000154: EC84002A  fadds      fp4,fp4,fp0
		00000158: 4200FF98  bdnz       *-104                   ; $000000F0

  There is still room for improvement, of course, but I'd rather not use hand-optimized assembly
  for the time being.  NOTE: Other effect loops (like MLRGBTransform) would probably benefit from
  vector instructions, but AFAIK, the CodeWarrior C compiler doesn't generate AltiVEC instructions
  (except perhaps saves/restores), and I don't have the time to look into the vBLAS library.

• 20021118: We now generate "huge" (up to 653x653) thumbnails from masters, and smaller thumbnails
  from the huge thumbnail.  Also, all on-screen manipulations (rotations, zooms, color adjustments, etc.)
  are now performed on the huge thumbnail, and the master is only reloaded at spool time.
  This can make a big difference in responsiveness when dealing with large (> 1 megapixel) pictures.

• 20021118: Interspersed a bunch of MLMacThread::Yield() calls within the thumbnail-creation code,
  in an attempt to make the UI thread more responsive.

• 20021119: Fixed several problems created by the introduction of "working canvases" of intermediate
  size (smaller than masters, larger than thumbnails).  One important thing to remember is that the
  affine transform needs to be adjusted when applied to a scaled-down canvas, by scaling the "offset"
  portion of the matrix.  The new MLAffineTransform::ScaleOffset() does just this.
  I removed the "thumbnail_huge" folder from the album folder layout, since it has never been used
  and I have no plans to use it in the future.  This is what the album layout looks like as of today:
    "master"			Imported master files go here, and are never touched.

    "work"				High-quality, JPEG-compressed, scaled-down (653x653) working versions of masters.
    					No futher transformations are applied to these files.  Transformed canvases in
    					the one-photo viewer are generated on the fly from these files, as well as all

	"thumbnail_large"	These are normal-quality, JPEG-compressed thumbnails of various sizes used in
						the 4-, 9- and 16-photo viewers.
						They are saved with all transformations pre-applied, including affine transforms
						(translation, rotation, scaling), color adjustments (contrast, brightness, saturation,
						etc.), frames, icons, etc.  They are regenerated automatically as necessary.

	"spool"				Printer-ready spool files go here.  They are always rearranged in landscape format
						(width > height), regardless of the orientation of the source photos.

• 20021119: Implemented a number of keyboard equivalents.
  The up-arrow and down-arrow keys can be used to switch viewer groups.  The down arrow cycles
  through the 1-, 4-, 9- and 16-photo viewers; the up arrow does the same but in reverse order.
  The '2', '4', '6' and '8' keys can be used to pan the image in the corresponding direction.
  The '7' and '9' keys rotate the image.  The '=' key returns the image to its original state by
  resetting the affine transform, all color adjustment values and removing the frame, if any.
  The '+' and '-' keys zoom the image in and out.

• 20021119: Optimized MLAutoLevel :: EffectLoop32Bit().  The pixmap inspection loop is now much faster
  because all conditional branches have been removed.

• 20021119: New build posted to my web site:


• 20021120: The sliders were broken in OS X.  I looked into this and the problem turned out to be
  that the slider control on OS X ignores the thumbCntl message used by MLMacSlider::MouseDown()
  to get the limitRect used to calculate the correct scroll range.  The framework does this because
  it insists on performing its own mouse tracking rather than relying on TrackControl().
  I submitted this problem to the Carbon mailing list and Guy Fullerton of Apple replied with this:

		>On 11/19/02 11:18 AM, "Marco Piovanelli" <> wrote:
		>> To perform live tracking of controls with indicators
		>> (scrollbars and sliders), the wrapper class sends the
		>> control a thumbCntl message.  The control is supposed
		>> to respond by filling in an IndicatorDragConstraint record
		>> whose limitRect is used to calculate the correct scroll
		>> range.
		>> This technique has worked beautifully since I think at
		>> least System 7.0, and still works in Jaguar (10.2.2) for the
		>> scrollbar control, but not for the slider control.
		>> The slider seems to ignore the thumbCntl message.
		>That's correct. At WWDC 2002, one of our Control Manager tidbits was that
		>you should no longer rely on being able to send control messages to CDEFs.
		>This is mainly because system controls may no longer *be* CDEFs. In most
		>cases, the system controls expect to receive messages in the form of Carbon
		>The slider is no exception. It expects to receive the
		>kEventControlGetIndicatorDragConstraint Carbon Event, and it no longer
		>handles the thumbCntl message.
		>(In a future release, the scroll bar control will also not handle the
		>thumbCntl message.)
		>> Should I file a bug?
		>Nope :)
		>> Can anyone think of a workaround?
		>Manufacturing and sendind a kEventControlGetIndicatorDragConstraint Carbon
		>Event to the slider should work.

  I implemented the workaround suggested by Guy and everything is peachy now.
  Also, DigitalEmotionApp::CreateColorPalette() now relies on GetThemeMetric() to
  calculate the correct height of the slider bounding box (currently 16 pixels in
  OS 9, 22 in OS X).

• 20021125: Working on a 3D affine matrix class I'm going to need to implement a number
  of RGB space transforms, including saturation and hue rotation.
  Among the many online resources that can be used to grasp the mathematical underpinnings
  of affine matrices, Eric Weisstein's World of Mathematics (now part of Wolfram's website)
  can be invaluable, and provide countless occasions of digression.


  More links:

  Gotcha: when pressing the clockwise rotate button in Digital Emotion, a *counterclockwise*
  rotation is concatenated to the current transform matrix.  Why is this?  It took me some time
  to figure out.  The reason is that in the Quickdraw coordinate plane, the positive y axis
  points down, i.e., in the reverse direction with respect to the familiar Cartesian plane.
  In other words, the Quickdraw plane is a standard Cartesian plane mirrored about a horizontal
  line crossing the image.

• 20021126: Added two mirroring functions: DigitalEmotionPhoto::FlipHorizontally() and
  DigitalEmotionPhoto::FlipVertically().  These are triggered by the 'h' and 'v' keys respectively.

• 20021126: Implemented saturation and hue rotation.  Reimplemented contrast and brightness in terms
  of 3D affine matrices.  Contrast, brightness, saturation and hue adjustments are concatenated in
  a single 3D affine matrix that is then applied to the working canvas by the MLRGBTransform effect.

  Optimized MLRGBTransform::EffectLoop32Bit().  Spent some time trying to devise a clipping function
  (to clamp a signed color value in the range 0 to 255) that uses as few PowerPC instructions as
  possible and avoids unnecessary conditional branches.  The Google Usenet archives reveal a great
  deal of discussion on this topic in newsgroups such as
  See this thread dating back to 1998, for example:


  In the end, I renounced using hand-tuned PowerPC assembly for this, opting instead for this straightforward
		inline int clamp_to_0_255 ( int x )
			return	( x & 0xffffff00 ) ?			//	need clipping?
					( ( x < 0 ) ? 0 : 255 ) :		//	yes, so return 0 if negative, 255 otherwise
					x ;								//	x is within allowed range

   Which generates fairly efficient code in CodeWarrior, albeit with a (single) conditional branch.
   This is what the non-inlined disassembly looks like:

		clrrwi.    r0,r3,8
		srawi      r3,r3,31
		li         r0,255
		andc       r3,r0,r3

• 20021127: Refactored DigitalEmotionApp::Listen() method.

• 20021128: Meeting with Leonardo Pellegrini, who volunteered to work on the Windows port of Digital
  Emotion early next year.
  Added saturation and hue sliders to the color palette.

• 20021202: Added startup checks for system version and CarbonLib version.  Digital Emotion will now
  refuse to run on Mac OS Classic if the system version is less than 9.2 and the version of CarbonLib
  is less than 1.6.  On Mac OS X, it will require version 10.1 or newer.

  Added a simple expiration mechanism.  Digital Emotion will refuse to run after an (internally hard-coded)
  expiration date, currently set to February 15, 2003.  It will also warn the user when the expiration
  date is less than a week away.

  Added robustness check in MLMediaCache dtor: a warning will be issued if any leftover cacheables (with
  a nonzero reference count) remain after the media cache is destroyed (ML_DEBUG only).
  Added Get() methods to MLMediaCache.  These are identical to the Acquire() methods, but don't increment
  the reference count of the returned object.

  Plugged a memory leak in DigitalEmotionPhoto::LoadMasterCanvas().  The reference count of the master
  canvas was incremented once too many.

• 20021206: First stab at implementing overlays: items that can be superimposed on the main canvas of a
  photograph, like text and icons.  These are relatively complex objects, with their own relative position,
  affine transform, z-order and alpha.  The photo compositor (DigitalEmotionPhoto::ComposeCanvas) draws
  the overlays, in back-to-front order, after the main transform and all color adjustments have been
  applied, but before the photo frame (if any).  Overlays form a class hierarchy rooted in the DigitalEmotionOverlay
  abstract class.  There are currently two concrete implementations: DigitalEmotionIconOverlay and
  DigitalEmotionTextOverlay.  Overlays can be selected, rotated and dragged around.  A new
  DigitalEmotionOverlayView class (a leaf MLView) conveniently binds an overlay object to the UI
  visual hierarchy.  Overlay views are created and destroyed on the fly as an album is browsed in one-photo
  viewer mode: they are the frontmost views in the main card, and live above the panner.

• 20021207: Redone the button file for the main viewer (viewer.btn) using the new rollovers I got from
  Marco S.  Several pieces, in the form of PhotoShop (.psd) files were combined into a single viewerf.psd
  file, then converted to PNG format and optimized using my own pngopt utility.

• 20021208: Split DigitalEmotion.cpp, which was starting to get a little too big for my liking, into several
  files: DigitalEmotionAlbum.cpp, DigitalEmotionPhoto.cpp and DigitalEmotionDrawable.cpp.  DigitalEmotion.cpp
  is still the place for the implementation of the DigitalEmotionApp class.

• 20021210: I need a new effect class that can apply a 2D transform to an icon pixmap and copy the transformed
  image to the working canvas of a photo using the icon alpha channel, all in a single step.  Unsurprisingly
  enough, I'm going to call this new effect MLAffineTransformWithAlpha.  In order to do this, I need to relax
  some assumptions made by the MLEffect base class: in particular, the source and destination canvases can now
  be different sizes.  This means EffectLoop16Bit() and EffectLoop32Bit() need to take two scanLineSize parameters
  rather than one.

  Working on MLFloatRect and MLFloatPolygon classes.

  Overhauled the whole DigitalEmotionOverlay hierarchy.  The base class can now perform much of the work,
  including keeping track of transforms, responding to zoom and rotate requests from the application,
  calculating bounding boxes and bounding polygons for hit-testing and highlighting purposes, and drawing
  the overlay.  The derived classes must implement GetOverlayCanvas().

• 20021211: Moved media root from "Digital" to "",
  which seems more appropriate.

  MLPropertyListWriter: updated format to plist version 1.0 from 0.9 (there are no changes AFAICT).
  The generated XML file now includes a <!DOCTYPE> declaration and a comment with the timestamp.

  DigitalEmotionOverlayView now performs precise hit-testing by delegating MLView::Contains() calls from the
  view hierarchy to a new DigitalEmotionOverlay::Contains() method that returns true on opaque pixels only.

  Swapped the positions of the contrast and brightness sliders, as per Marco's request.

  Added a new DigitalEmotionColorWell class -- a simple leaf view, subclassed from MLAbstractButton, that
  can be used to select a predefined color.  Several such views (13 at the time of this writing) are gathered
  in a radio group in the text palette.

• 20021212: Made new build available on my website:


• 20021212: Implemented font popup menu in the text palette.  Oh, and while I was at it, I also made some
  extensive modifications to the MLMacControl class, which was really thought for controls with indicators
  like scrollbars and sliders, but wasn't really up to snuff for wrapping other Macintosh controls.

• 20021215: Modified DigitalEmotionAlbum::SpoolPhoto() so that the aspect ratio of spool files is calculated
  accurately.  Previously, any photo would be stretched to fit the largest possible printable area (see 20021021
  entry in this log).

  Introduced a new DigitalEmotionLayout class.  A layout is a collection of overlays (icons and text), plus an
  optional frame.  Moved just about all the overlay-related functionality in DigitalEmotionPhoto to DigitalEmotionLayout
  and made DigitalEmotionLayout a base class of DigitalEmotionPhoto.

  Initial implementation of layouts.  To create a layout, add overlays and an optional frame to a template photo,
  then switch to Layout mode (by pressing the LAYOUT button) and hit the Enter key.  A new layout thumbnail will
  be added to the Special bar.  To apply a layout to the selected photos, just click the corresponding thumbnail
  in the Special bar.

• 20021217: Added DigitalEmotionOverlay::PreciseHitTesting() method.  This method is invoked by DigitalEmotionOverlay::Contains()
  to determine whether accurate hit-testing should be performed.  When this method returns true, only opaque points
  in the overlay (points with a nonzero alpha) are hits; when it returns false, all points within the overlay canvas
  bounds are hits.  The default implementation of PreciseHitTesting() return false.  DigitalEmotionIconOverlay overrides
  this method to return true for icons.

  Layout thumbnails now have a white, rather than black, background.

  Newly created overlays are now automatically selected.

  Closing the text palette (by clicking the window close box) is now equivalent to pressing the PHOTO button.

• 20021218: As per specifications, certain buttons need to operate in 'continuous' mode, i.e., they have to perform their
  associated 'click' action repeatedly while the mouse is being held down in their active region, and not just at mouse-up
  time like regular buttons.  To implement this, I added DigitalEmotionTrackingButton, a new subclass of MLButton that
  also derives from MLPeriodical.  A DigitalEmotionTrackingButton is like a regular button, but starts Click()ing itself
  after a certain delay (default = 500ms) has elapsed since mouse-down.  Once this delay has passed, the tracking button
  keeps clicking itself every 100ms.  Since buttons are instantiated automatically by parsing .btn files, I also had to
  subclass MLButtonFileConverter with a new DigitalEmotionButtonFileConverter class, which queries DigitalEmotionApp::IsTrackingButton()
  to determine whether it should create a DigitalEmotionTrackingButton or a regular MLButton.
  Currently, IsTrackingButton() return true for the rotate buttons (BTN_viewer_ROTATECW, BTN_viewer_ROTATECCW) and for the
  zoom buttons (BTN_viewer_PLUS, BTN_viewer_MINUS).

• 20021219: Working on photo labels -- MLStaticText objects that show the serial ID of photos in viewers.

• 20021223: Working on the print queue window.  MLListView needed several changes to support lists with multiple columns.

• 20021225: Working on print queues.  The print queue window has a picture view for displaying thumbnails of pending print
  jobs and a list view with four columns (job ID, status, creation date, spool file size).  Four buttons at the bottom of
  the window allow to suspend, resume, and delete jobs, and to flush the whole queue.  Each job in the queue can be in one
  of seven states:

		1:		kStatusSpooling						The spool file is being created.
		2:		kStatusCreatingThumbnail			The thumbnail for the print job (120x120) is being created.
		3:		kStatusReady						The job is ready to print -- waiting for a printer to become available.
		4:		kStatusSuspended					The job has been suspended -- it has to be manually resumed in order to print.
		5:		kStatusPreparing					A printer is being prepared to print this job.
		6:		kStatusUnspooling					The spool file is being uploaded to the printer.
		7:		kStatusPrinting						The printer has begun printing.  We're finished with this job.

  Ready jobs can be suspended, and suspended jobs can be resumed.  Jobs in other states can't be altered.
  A new subfolder of the album folder (thumbnail_print_job) holds 120x120 thumbnails of the spool files meant to be displayed
  in the print queue window when the corresponding job is selected in the queue list.

• 20021228: Added drag & drop support to MLListView.  A new bool parameter to the standard constructor can be used to specify
  a list as draggable.  A draggable list can be a drag source and/or a drag sink.  When it's both at the same time, the drag
  gesture is interpreted as an attempt to rearrange lines, and a kCommandListLineMoved is broadcast.  The parameter to this
  message is a pointer to a MLListView::LineMove structure, which includes the index of the moved line and its new position.

  The print queue window takes advantage of this new functionality to allow rearranging of pending print jobs.

  Bug fix: It looks like some Toolbox routines (TrackDrag seems to be one) change the current graphics port behind our back.
  This confuses MLUseCanvas, and can cause it to fail to set the graphics port correctly.  I added some debugging code (only
  compiled when ML_DEBUG == 1) to detect this problem, and also added a public static MLUseCanvas::ResetCanvas() method meant
  to be invoked immediately after any system API that may change the current port.

• 20021230: Added support for photo sets.  A photo set is just a list of photos from an album.  An album can have several
  photo sets, but at any given time, only one of them is current: the so called "working set" (WS).  Photo sets are arranged
  in a double stack, akin to the double stack that's often used to model a web browser page history or multiple undo/redo.
  Initially, the "backward" stack contains only one element, the complete photo set containing all the photos from an album.
  The CLEAN button creates a new photo set containing the selected photos -- this new set is pushed onto the backward stack
  and becomes the new working set.  Each new set created by CLEAN is a subset of the previous one.  The PREV WS button pops
  the backward stack, makes the second-from-top element the new working set, and pushes the popped set onto the "forward"
  stack.  The NEXT WS button performs the reverse operation, moving the top of the forward stack to the top of the backward
  stack.  Doing a CLEAN empties the forward stack, i.e., immediately after a CLEAN, the NEXT WS button has no effect.
  There is no hard limit on the depth of the working set stack.

  The PREV WS and NEXT WS buttons are superimposed over the regular PREV / NEXT buttons, and kept hidden most of the time.
  They're only revealed if the option key is held down.  The application registers a kEventClassKeyboard/kEventRawKeyModifiersChangedCarbon
  event at startup so it's automatically notified when the state of the modifier keys is changed -- no need to poll the keyboard
  using a periodical (a timer) as was the case in the pre-Carbon world.

  Photo sets are not currently part of the persistent state of an album, i.e., they're not saved to disk.  On re-opening
  an album, the working set is set to the entire collection of photos.

  Bug fix: worked around a subtle reentrancy issue in MLEmbeddingView::DispatchMouseUp().  DispatchMouseUp() used to call
  its clicked subview's MouseUp() method *before* clearing its mClickedView member.  In some cases, MouseUp() could trigger
  actions that caused other views to be shown/hidden or activated/deactivated.  This, in turn, forced a cursor redispatch
  (which is needed in case views under the mouse change their visibility or activation state) that could eventually cause
  the clicked subview's MouseTrack() method to be re-entered before the completion of MouseUp().

• 20021231: Made a new build available at:


• 20021231: Working on keyword families.  Text files in the Common Keywords folder are now read by a scanner thread
  (DigitalEmotionKeywordScanner) at startup.  Such files *must* have a .txt extension.  The file encoding can be either
  MacRoman (the default Macintosh encoding) or Unicode (UTF-16).  Unicode files can be either big-endian (Macintosh, Unix)
  or little-endian (Windows), but *must* begin with a byte-order mark (BOM) to be properly recognized as UTF-16-encoded.

• 20020102: Due to the new, much cheaper implementation of MLUseCanvas (see entry 20021114 in this log), I have to watch
  out for cases where the framework calls an external API that changes the port then invokes a framework callback before
  putting it back.  Such situations tend to mess up the current port, so we end up drawing in the wrong window.  One place
  where this happens is MLMacText::Draw(), which calls WEUpdate(), which in turn invokes the MLMacText::EraseProc()
  callback, possibly with a different port.  The easy workaround consists in adding a MLUseCanvas declaration at the
  beginning of MLMacText::Draw().  How I wish the original designers of Quickdraw had APIs take an explicit port parameter,
  rather than having it passed implicitly using a global.  Very bad design, and lots of headaches for generations to come.
  This is one instance where the designers of the Windows API were smart enough to avoid copying an obvious design flaw
  from the mac: GDI calls take an explicit device context parameter.

• 20030103: DigitalEmotionAlbum::RestrictWorkingSet() now takes a bool parameter specifying whether the selection should
  be cleared.  The default value is false, so selected photos remain selected after a CLEAN.

  Implemented TRASH command for photos, icons, frames and layouts:
  	1. In PHOTO mode, TRASH removes the selected photos from the album and deletes all the associated on-disk files
  	   within the .album bundle.  This means both the master image file and all thumbnails.  An alert box asks for
  	   user confirmation before proceeding, and reports the number of selected photos.

    2. In ICON and FRAME modes, TRASH temporarily removes the selected items from the special bar.  No files are
       deleted.  The "trashed" items re-appear as soon as the album is closed and re-opened.

    3. In LAYOUT mode, TRASH permanently removes the selected layouts from the special bar and from the current album.
       Only the layout is removed, not any associated frame or icons.  No files are deleted, and no confirmation alert
       box is displayed.

• 20030105: Split the SELECT ALL button into two different buttons: SELECT ALL and DESELECT ALL (which should probably
  be named SELECT NONE -- I think it sounds better).  The number of selected photos at any given time is now displayed
  in the relevant area at the top of the viewer card, using the Architecture font to match the look of the user interface.

• 20030109: Fixed bug whereby creating a new layout would cause an assertion failed error.

  Clicking the TRASH button in PHOTO mode now moves the master files to the trash, rather than deletes them immediately.
  Associated thumbnails and other working files, on the other hand, are deleted immediately.

• 20030120: Fixed another failed assertion (currentPort == cCurrentCanvas) seen when typing in text fields.

  Optimized various MLCanvas methods in Carbon targets by having them call Quickdraw APIs that take an explicit port
  parameter, instead of older, equivalent APIs that refer implicitly to the current port.  For example, MLCanvas::GetClipRgn()
  now uses GetPortClipRegion() rather than GetClip().  This avoids a number of occurrences where the Quickdraw port had
  to be set and restored using stack-based MLUseCanvas objects.

  In Carbon targets, we now use FMGetFontFamilyFromName() and FMGetFontFamilyName() instead of GetFNum() and GetFontName(),
  as per Apple's recommendations.  It should be noted, however, that the newer APIs are only available in Mac OS 9.0 and newer.

  Digital Emotion will now always display a confirmation dialog when the TRASH button is clicked, not just in PHOTO mode.

  Marco S. says layouts should adapt to the dimensions of the photos they're applied to.  This means we should scale both
  the positions and sizes of all overlay elements before applying them.  In order to do this, we need to record the size of
  the original template photo used to create the layout.  So, I'm moving the member variables mWidth and mHeight from
  DigitalEmotionPhoto up the hierarchy to DigitalEmotionLayout.

• 20030129: Extended expiration date to March 15, 2003.
  Added dimmed (inactive) rollovers for all buttons to viewer.btn.  Added code to dim buttons when they're not applicable.
  For example, PLUS, MINUS, ROTATE CLOCKWISE and ROTATE COUNTERCLOCKWISE are only available in single viewer mode, so they're
  now dimmed in 4/9/16 modes.

• 20030201: Working on slide shows.  Added Stop Slide Show button.  Created a new DigitalEmotionSlideProjector class
  (a subclass of MLPeriodical), which shows a set of photos in a specified viewer, changing photos at a given interval.
  Created MLPeriodicalAction class: this is an action that can be used to activate or deactivate a given periodical.
  Changed the terminology used by the MLPeriodical class, substituting the more accurate "interval" for "frequency":
  the quantities at issue are in fact times, measured in milliseconds.
  A new XML key in the preferences (slide_projector_interval) allows setting of the default interval used by the slide
  projector.  This is a real value expressed in seconds.  The default value is 5.0.

• 20030202: More code to handle button activation & deactivation: TRASH, CLEAN, PREV, NEXT, etc.

• 20030203: Changed DigitalEmotionOverlayView::MouseDown() so that clicking an overlay gives it the focus.  This allows
  the user to delete an overlay when the text palette is shown (by clicking it and hitting backspace), which was previously
  impossible.  To return the keyboard focus to the text palette, just click the text field.

  Layouts now adapt to the dimensions of the photos they're applied to.  I added a DigitalEmotionDrawable::Scale(float, float)
  method that allows each axis to be scaled independently.

  In single viewer mode, overlays can be manipulated regardless of the layout setting (PHOTO, FRAME, ICON, LAYOUT or TEXT).
  I'm changing this, as per Marco S.'s suggestion, so that when in ICON mode, only icons can be clicked, and when in TEXT
  mode, only text overlays can be clicked.  I don't particularly like this change, though.  I think mac apps should try to
  be as modeless as possible, as the Apple's HIGs recommend, and I don't see any particular reason why all overlays should
  not be selectable at all times.

  Made new build available on my web site:

• 20030207: DigitalEmotionPhoto::Reorient() wasn't updating the photo dimensions correctly, and this caused problems
  with layout application.  Now it swaps width and height.

• 20030212: Working on keywords.  Added several methods to the MLMacControl class: SetTitle(), GetTitle(), GetBestRect(),
  SetTextColor(), SetBackgroundColor(), SetFontStyle() and SetFont().  Added MLWindow::SetBackgroundColor() (MacOS only).

• 20030213: Working on keywords.  The SET KEYWORDS dialog (actually just an MLWindow with the kModal flag set) must be
  set up for a given photo set using DigitalEmotionApp::SetUpKeywords().  Dismissing the dialog causes DigitalEmotionApp::
  ApplyKeywords() to be executed, which applies changes to the original set.  Each family popup menu has a "none" entry
  (which can be used to remove keywords from that family) and a dimmed "mixed" entry, which is selected by SetUpKeywords()
  when not all photos in the set have the same keyword for the given family.

  Photo volumes (Compact Flash cards, etc.) imported into an album are now automatically assigned a unique (album-wide) serial
  number, called the "media ID" of the volume.  Each imported photo is tagged with this media ID.

  Implemented progress bar.  In order to display a determinate progress bar, we need to scan inserted volumes twice: once
  just to count the graphics files, and again to actually import the files.  Changed return value of DigitalEmotionScanner::
  ScanComplete() from void to bool -- returning true amounts to requesting a re-scan of the volume.  The default method returns
  false; the DigitalEmotionImporter subclass returns true.

• 20030215: Rewritten the code for the SET KEYWORDS dialog to use standard Macintosh buttons.  New MLMacButton class: a simple
  subclass of MLMacControl with a couple of additions to better support standard pushbutton behavior.  Redesigned the SET
  KEYWORDS dialog so that popups are now laid out horizontally on a single row, rather than vertically.  This was done so
  the SET KEYWORDS dialog more closely resembles the Find dialog that I'm going to tackle real soon.

• 20030217: Revised MLMenu.cpp for better Carbon compatibility.  Removed ML_MACOS_SUPPORT_ALLEGRO guards as I think I can now
  safely assume the framework will never be used on anything older than Mac OS 8.5.  New MLMacPopupMenu class: another simple
  subclass of MLMacControl meant to better encapsulate the standard popup menu control.

  Started implemented search functionality.  The DATABASE button brings up a "Find" dialog, which shows a matrix of keyword
  family popups.  Initially, the matrix has only one row, containing one popup for each keyword family.  Hitting a PLUS (+)
  button allows the user to append more rows.  Likewise, the MINUS (-) button on the right of each row allows the user to
  remove that row.  This design was inspired by the Find window in Jaguar's Finder.

  Several new types to help with searches: DigitalEmotionCriterion represents the simplest possible search criterion
  (is keyword X = Y?).  DigitalEmotionCriterionRow represents a row (list) of such criteria, ideally stringed together by
  AND operators: a photo satisfies a DigitalEmotionCriterionRow if and only if it satisfies all criteria in the row.
  DigitalEmotionCriterionMatrix is a list of criterion rows, with rows ideally stringed together by OR operators: a photo
  satisfies a matrix if and only if it satisfies at least one row in the matrix.

  DigitalEmotionApp::GetCriterionMatrix() examines the Find dialog and builds the corresponding criterion matrix.
  DigitalEmotionAlbum::MatchWorkingSet() takes a criterion matrix and builds a list of matching photos.  The search space
  can be the whole album, or just the current working set.  Marco S. says the latter option is not necessary.

  Made new build available on my web site:

• 20030218: Added MLMacClockControl class, a simple wrapper around the Macintosh Clock control.

  Added mac-specific constructors and accessors to MLDate for easy conversion between MLDate and the DateTimeRec and LongDateTime
  data types.

  New MLGraphicsExporter accessors: GetExifEnabled(), SetExifEnabled(), GetThumbnailEnabled(), SetThumbnailEnabled().

  When loading the master canvas, a new DigitalEmotionPhoto::ExtractMetaData(const MLQTUserData&) method is called, which
  allows the photo to easily extract EXIF tags stored in JPEG and TIFF files and recognized by the QuickTime importers.
  Currently, the only EXIF tag I extract is DateTime (EXIF 0x132), which corresponds to the QuickTime kUserDataTextCreationDate
  constant.  This tag is usually associated with the time when the photograph was taken by the camera.  When this information
  is present, it is replicated in the manifest.plist file as part of the photo dictionary, under the key "shoot_date".

  Thumbnail files created by Digital Emotion now contain EXIF tags indicating the file creation date, the version of QuickTime
  used to generate the file, the operating system version, and the build date of Digital Emotion.

• 20030221: Updated James Clark's "expat" XML parser to version 1.2.

• 20030223: Working on date searches.  Added DigitalEmotionAlbum::GetOldestShootDate() and DigitalEmotionAlbum::GetNewestShootDate().
  These are used to initialize the clock controls in the Find window to a date range encompassing all photos in the current
  album (or working set).  Added MLDate::MergeDateParts() static utility.  This is used to merge dates coming from two clock
  controls, a kControlClockTypeMonthDayYear one and a kControlClockTypeHourMinute one.

  Made new build available on my web site:

• 20030223: Added several Finder-related resources needed by the classic Mac OS (BNDL, FREF, open, kind, ICN#, icl8, ics#, ics8, etc.)
  On Mac OS X, these resources are superseded by the Info.plist XML file and by the .icns files.
  Added code to turn album folders into packages recognized by the Mac OS 9 Finder.  Basically, the kHasBundle bit must be set
  on the .album folder and an alias file must be created at the root level of the album hierarchy.

• 20030223: Added code to perform safe saves of the manifest.plist file.  "Safe" save means that we first write the XML data to
  a temporary file and then (when all the data has been written and the file is closed) we exchange the temporary file with the
  original file.  Previously, if something went wrong during the save, the manifest file could be corrupted or destroyed, making
  the album unusable.

• 20030224: Changed startup sequence.  Instead of opening the "default" album, Digital Emotion will now display a standard "Open"
  dialog to let the user pick an existing album.  If this dialog is canceled, Digital Emotion will display a standard "Save"
  dialog to allow the user to create a new album.

• 20030224: Wouldn't it be nice if our standard MLRadioGroup embedder could work with standard Macintosh radio buttons?
  Sure it would, but right now, radio buttons must inherit from MLAbstractButton in order for MLRadioGroup to use them,
  and MLMacButton's are not part of that hierarchy (and probably rightly so).  So to work around this problem I introduced
  a new lightweight interface class, MLAbstractRadioButton -- basically a view that implements the SetSelected(bool) protocol.
  Both MLAbstractButton and MLMacButton now implement this interface, unless the ML_RADIOGROUPS compile-time switch is set to 0
  in the prefix file.  While I was at it, I added auto-toggle functionality to MLMacButton, similar to the existing auto-toggle
  feature in the MLAbstractButton class.  This is useful for checkboxes and radios.

• 20030224: Working on the Album Options window.  This window is displayed when a brand new album is created, and allows setting
  of such options as "delete after import" and default orientation.  The "delete after import" setting (default = false) determines
  whether photos on camera cards should be automatically deleted as soon as they're copied to the album.  The default orientation
  setting determines whether imported photos should be reoriented, and how.  By default, photos are left in their landscape
  orientation.  Two additional options allow automatic rotation (90 clockwise or counterclockwise) upon importing.

  Made new build available on my web site:


• 20030225: Found a nasty bug in MLString::EndsWith() and MLUniString::EndsWith() that must have plagued the ML framework for
  years without anyone noticing.  Basically, s.EndsWith(t) would incorrectly return true if (s.Length() == t.Length() - 1).
  This caused files with 3- or 4-character names to be accepted by DigitalEmotionImporter::IsGraphicsFile(), regardless of
  extension, triggering a -2003 (cantFindHandler) OS exception in MLGraphicsImporter.
  I fixed this and rewrote all variants of BeginsWith() and EndsWith(), as the original ones were very inefficient.

• 20030225: Changed the naming scheme for photo files to use 5 digits ("00001.jpg") instead of 4 ("0001.jpg").
  This raises the maximum theoretical number of pictures in an album from 9,999 to 99,999, although I guess other portions
  of Digital Emotion may show their limitations before this ceiling is hit.

• 20030226: Investigated techniques that can be used to implement a registration scheme based on "activation codes".
  Cf. <>
  Downloaded sample code by Rich Kubota to retrieve the built-in Ethernet (MAC) address, cleaned it up and added to
  the "Mac OS Extras" folder of the framework as "GetEthernetAddress.cpp".

• 20030227: Added a Save command to the File menu (cmd+S) to allow saving an album at any time.

  The Keywords window is now displayed after importing pictures using the IMPORT button.

  Added white 1-pixel border to one-photo viewer.

  Tweaked the photo set mechanism in several ways.  First of all, when an album is created, *two* photo sets are allocated:
  the base set (set 0), which holds all of the photos in the album, and the first user-visible set (set 1) which
  holds the photos the user can see, and is initially empty.  The base set is now unreachable, i.e., you can't go back
  to set 0 using the PREV WS command.  This means an album *always* has at least two sets (0 and 1).  The Find and Find
  All commands (implemented by the MatchWorkingSet() and ResetWorkingSet() methods, respectively) alter set 1, but never
  set 0.  Conversely, newly imported photos are only added to set 0 (previously, they were added to all existing sets
  indiscriminately), and therefore are no longer immediately shown as they become available.

  Added compile-time switch to control the encoding of XML files generated by MLPropertyListWriter, which up to now
  could only create UTF-16-encoded files.  Now, files are output in UTF-8 by default, but UTF-16 is still available by
  setting #defining ML_WRITE_UTF16_PLIST to 1 in the prefix file.  This change allows Digital Emotion to generate manifest
  files that are half the previous size.

  Buffered plist I/O!  This means the manifest file is now read and written using a fixed, very small amount of memory
  (a few K), regardless of the actual file size.

• 20030228: Added MLGraphicsImporter::SetProgress(), which mirrors MLGraphicsExporter::SetProgress().
  Renamed and moved to global scope the MLGraphicsExporter::Progress class, which is now named MLGraphicsProgress.

  Moved some initialization/termination code to a new file: DigitalEmotionInit.cpp file from DigitalEmotion.cpp,
  since the latter is becoming big and unwieldy.

  Made new build available on my web site:


• 20030303: Working on HTML export.

  More system information is now written to the console in debug builds: physical RAM size, CPU type, CPU clock speed
  and bus speed.  Also, the Blue Box (aka Classic compatibility environment) is now detected.

  Bug fix: Deleting a photo in an album living on a volume other than the startup disk would fail with a -50 error.
  Why?  Because DigitalEmotionPhoto::MoveToTrash() would try to move the master image file to the trash _of the startup
  volume_, and moving files across volumes is obviously doomed to fail.  This bug uncovered a design weakness in MLPath:
  the MLPath(ESpecialPath) constructor does not have a way to specify the volume for those few cases where the special
  path is volume-specific.  From a cursory glance, the special cases seem to be: the trash, the desktop and the temporary
  items folder.  I changed the relevant constructor to take an additional, optional inVolumeHint parameter.

• 20030304: Albums are automatically saved after automatic or manual import, and deletion, of pictures.
  Slide shows now loop by default -- the DigitalEmotionSlideProjector ctor takes an additional inLooping parameter that
  controls this behavior.  The Keywords window is now *really* displayed after importing pictures using the IMPORT button
  (I had thought I did this on 20030227, but I forgot to add a Show() call).
  Inserting a volume containing no suitable graphics files causes an "Empty Card" alert box to be displayed.

  Worked around a subtle problem with the way MLPictureView loads and unloads images when it's shown and hidden -- this
  caused the one-photo viewer to remain black at the end of a slide show.  What was going on here?  Well, the slide show
  projector lives on a cardview of its own; when this cardview is activated, the main one gets hidden along with all its
  subviews; the MLPictureView::Hide() method releases its mImage field; when the main card is later reactivated at the
  end of the slide show, the MLPictureView::Show() method is invoked, but the image is gone and can't be reloaded from
  file because there is no backing file: unlike smaller thumbnails, this image is composed dynamically.  The slightly
  kludgy workaround I used is to call RepaintPhoto() from DigitalEmotionApp::EndSlideShow(), which causes a new image
  to be synthesized and assigned to the one-photo viewer.  Also, I tweaked MLPictureView::ReplaceImage() so that it
  retains the image even when hidden (this constituted a potential leak, since the image was Acquire'd but its reference
  discarded), and MLPictureView::Show() so that it always recalculates the opaque rect, even for non-file-based images.
  NOTE: this workaround does NOT cover the non-looping case, when the slide show ends without user intervention.

  Added an empty entry to the Date Operator popup menu in the Find window.  This entry, corresponding to kBooleanOperatorNone,
  simply tells the matching logic to ignore the date range specified in the dialog.

  In order to guarantee maximum compatibility with various property list parsers (notably Apple's Property List Editor),
  the MLPropertyListWriter class will write dates in the "yyyy-mm-ddThh:mm:ssZ" format, instead of "yyyy-mm-ddThh:mm:ss"
  as it's been doing so far.  Note the extra "Z" in the new form.  This "Z" states that the date is expressed in Universal
  Time (UTC) -- in other words, it affirms the time zone for the date is Greenwich Mean Time (GMT).  In fact, MLPropertyListWriter
  has no way to enforce or even verify this constraint, since the MLDate data type does not carry time zone information with
  it.  In such a case, ISO 8601 would recommend no time zone suffix is added, but the Apple plist format appears to be
  stricter and to always require the "Z".  Cf:

		<>			[PLIST]
  		<>				[ISO-TIME]

• 20030305: Implemented a new MLBitVector class.  This is basically an optimized array of bools, where each item takes
  up just one bit of memory.

• 20030309: Implemented generic Base64 encoding/decoding routines, wrapped in a MLBase64 class.
  For the official definition of the Base64 encoding scheme, see RFC 2045, "Multipurpose Internet Mail Extensions (MIME)
  Part One: Format of Internet Message Bodies" (November 1996):

  		<>						[RFC2045]

• 20030310: Extended my plist parser/writer so that it understands and writes "data" values (see [PLIST]).  These are
  unqualified blobs of binary data which are written to the XML file using the Base64 encoding scheme.  The Base64 data
  is split into lines of 64 characters each for readability.

  A new constructor and a new accessor allow MLBitVector to be externalized to a MLBuffer.  With this last step, I can
  now change the keyword code so that multiple keywords from the same family can be assigned to a single photo (as per
  Marco S.'s untimely request).
  I changed DigitalEmotionKeywordMap from MLMap<UInt32, UInt32> to MLMap<UInt32, MLBitVector>: the mKeywordMap member
  of DigitalEmotionPhoto now maps family IDs to bit vectors, containing one bit for each keyword in the family.

  I declare the MLUseCanvas experiment begun last November (see 20021114 entry) officially failed.  There are way more
  times when the Quickdraw port might be changed behind my back than I care to watch out for.  I reverted back to the
  old, straightforward GetGWorld/BeginDrawing/EndDrawing/SetGWorld code.  I removed MLUseCanvas::cCurrentCanvas and

• 20030311: Implemented two-photos-per-sheet printing.  Added DigitalEmotionAlbum::Spool2Photos() and
  DigitalEmotionAlbum::PlaceOntoPrintCanvas().  NOTE: unlike one-photo printing, which uses Quickdraw's CopyBits()
  in ditherCopy mode to stretch the picture onto the printing canvas, Spool2Photos() relies on "placement matrices"
  and the MLAffineTransform class to place pictures on the printing canvas.  This may entail a slightly lower quality
  of the output, at least until I update the MLAffineTransform class to offer a better interpolation algorithm.
  Implemented print logging.  Jobs that are successfully printed (or more exactly, successfully unspooled to the
  printer) and now moved to a separate per-album print log, rather than deleted.  This will allow us to compile
  statistics and answer questions such as "how many A5-format pictures were printed since this album was created?".

  Implemented print log window.

• 20030312: The Keywords window can now have multiple rows, much like the Find window.  This allows setting of
  multiple keywords per family.

  Fixed a bug that prevented the thumbnail for the very first layout created for an album to be shown.

  The Find window is now reset to one row of default values each time the DATABASE button is clicked.

  The Color palette can now remain open in 4/9/16 modes, even though it will only actually do something in single
  viewer mode.

  The Prev WS and Next WS buttons are now dimmed appropriately.  Added new methods DigitalEmotionAlbum::HasPreviousWorkingSet()
  and DigitalEmotionAlbum::HasNextWorkingSet() for this purpose.

  Using the PREV WS button to go from set N to set (N - 1) now causes all photos in set N to get selected.
  In this scenario, CLEANing set (N - 1) is equivalent to going to the next working set by clicking NEXT WS.

• 20030313: Clicking option-PRINT when one photo is selected will print two A5 copies of the picture on a single A4 sheet.

• 20030320: More configurability: added three new keys to the preferences.plist file: "gutter_width", which specifies
  the width, in pixels, of the blank strip separating the pictures when printing two photographs on a single sheet;
  "min_volume_size" and "max_volume_size", which specify the range of capacities (in megabytes) a removable volume must
  be within to be eligible for automatic import of pictures.  The default values are:
  		gutter_width			80
  		min_volume_size			2.0
  		max_volume_size			130.0

• 20030324: GetEthernetAddress() now works on Mac OS X, using I/O kit to retrieve the MAC address of the primary
  Ethernet interface.

  Added "Hide & Switch to Finder" command to the File menu, with a command-H keyboard shortcut.

• 20030325: Removed support for Dean Yu's old floating window library.


  We now assume the base Mac OS system version is 8.6.  Removed several t-vector checks for APIs that are
  guaranteed to be available in Mac OS 8.6 or newer (e.g., InvalWindowRect, MenuEvent, FetchFontInfo).

  All Macintosh windows are now created using CreateNewWindow() instead of NewCWindow(), with one exception:
  full-screen windows (MLWindow::kFullScreen) are created with NewCWindow (with a plainDBox proc ID) in non-Carbon
  builds, since CreateNewWindow only supports kPlainWindowClass under Carbon.

  Added MLWindow::kMinimizable window attribute.  This attribute causes windows to have a collapse box in classic
  Mac OS (a minimize widget in Mac OS X).

  Upgraded to Universal Headers 3.4.2 (from 3.4.1).

• 20030331: Revisited some core framework classes, notably MLBuffer, MLArray and MLMap, for performance and correctness.

	+	MLMap takes a third, optional template parameter that allows you to specify a custom key comparison functor.
	+	Rewritten MLMap::PrivateLookup() so that Key::operator==() is no longer used.
	+	Rewritten MLMap::PrivateLookup() to reduce the number of calls to the comparison functor to about log2(N),
	    where N is the size of the map.  Previously, this number was more like 3/2*log2(N).
	+	Eliminated MLBuffer::Lock() and MLBuffer::Unlock().  These were remnants of the old days when MLBuffer was
	  	based on Macintosh Memory Manager handles.  Nowadays, MLBuffer uses malloc/realloc/free, and doesn't need a
	  	lock count.  You can now obtain a pointer to the underlying raw storage using MLBuffer::GetStorage().
	+	The MLUseBuffer stack-based helper class does almost nothing now, and should be phased out.  It's still
		defined because so much code relies on its existence.
	+	Eliminated MLArray::mRawStorage data member.  This was redundant, since given a MLArray<T> "x", x.mRawStorage
		was equivalent to static_cast<T*>(x.mBuffer.mStorage) at all times.
	+	MLArray::operator=() was badly broken: it failed to destroy existing elements and performed an unnecessary

  A new MLCaseInsensitiveStringCompare class provides a handy functor for creating string-keyed, case-insensitive
  MLMaps.  Revised MLMediaCache::CacheMap and MLHTTPRequest::HeaderMap to take advantage of this new feature.

• 20030412: Extended expiration date to May 19, 2003.

• 20030422: Fixed bug in DigitalEmotionApp::Create2ndScreenWindow(): the MLWindow::kAutoCenter flag was incorrectly passed
  to the MLWindow ctor, but this flag causes the window to be centered on the _main_ screen, which is not what we want.

• 20030430: Starting from today, Andrea Pelizzari ( will help me develop portions of
  Digital Emotion.

• 20030505: Set up a Perforce server (see <>) with the help of Leonardo Pellegrini.
  Since Digital Emotion is now being developed by a geographically distributed team, we needed some kind of version
  control system to keep track of changes, and I think Perforce fits the job nicely.  The coordinates of the Perforce
  server are:

  TODO: consider using SSH tunnels to protect Perforce connections from prying eyes.

• 20030506: Started working on a registration/activation mechanism for Digital Emotion.
  Digital Emotion will be sold/distributed with "serial numbers", to be "activated" during installation by performing
  an HTTP POST request on a remote registration server to be hosted by Ledsoft.  The POST request will include several
  parameters, including the serial number, some kind of ID which uniquely identifies the client machine (most likely,
  the 48-bit MAC address of the primary Ethernet interface) and a slew of system information, useful for statistics
  purposes (OS type, OS version, Carbon version, QuickTime version, CPU type and speed, physical RAM size, startup
  volume capacity).

  Added DigitalEmotionRegistration.cpp, MLMacSocket.cpp and MLHTTPRequest.cpp to the project file.
  Removed the classic target from the project file.

  A new DigitalEmotionSerialNumber class encapsulates a "serial number" and manages conversions between the
  human-readable sequence of alphanumeric digits and the underlying integer.

• 20030512 (working on the highway): Adding support for printing four photographs on a single A4-size sheet.

  Marco Silvestri sent me a wish list of 11 new bug reports and enhancement requests for Digital Emotion.
  Most of those are easily implemented, so I'm incorporating them in my to-do list for version 2 at no
  additional price.

• 20030513: Extended expiration date to July 7, 2003.

  Added support for keeping track of print statistics across all albums [entry #11 of 20030512 wish list].
  DigitalEmotionApp::UpdatePrintStatistics() is called as soon as a print job completes successfully.
  Added a new "A6" column to the Print Log list, showing the number of A6-size prints (four per sheet, count'em).
  Also added a new "Totals (All Albums)" row.

  Following a suggestion by Marco Silvestri, the Keywords window background is now light red rather than light gray
  to help operators tell it from the similar Find window [entry #7 of 20030512 wish list].

  According to Marco Silvestri, sometimes the debug version of Digital Emotion fails to launch the console server
  application, i.e., Style.  Just in case this is due to some desktop database corruption problem, I added a few lines
  to DigitalEmotionApp::DoInitialize() which scan the desktop databases of local volumes and remove any bogus entries
  for the console server.

• 20030514: Added support for changing the album options (currently, just "delete after import" and "default
  orientation") after the album is created.  This is done by choosing "Album Options..." from the File menu
  (only usable at screen resolutions above 1024x768) or by pressing command-, [entry #13 of v2 specs].

  Added "author" and "description" edit fields to the Album Options window [entry #14 of v2 specs].

  Implemented Print Options window.  To display this window, make sure only one photo is selected, and hold down
  the option key while clicking the PRINT button.  Available options currently include the job format (A4, A5 or A6)
  and the copy count.  Attempting to queue more than 10 copies of the same print brings up a warning message.
  This addition completes [entry #12 of v2 specs].

  Posted a new build to my website:

  The photo label in the one-photo viewer now has an associated tooltip displaying the date and time when
  the corresponding photo was shot, according to the Exif tags embedded in the master file.

• 20030517: Andrea implemented a new bicubic interpolation algorithm to use when applying affine transforms
  (zoom, rotation, etc.).  The bicubic algorithm is significantly slower than the simplistic nearest neighbor
  (zero-order) interpolation used so far, but produces excellent results: jaggies (aliasing) have virtually
  disappeared, without sacrificing high-frequency details too much, as was the case with previous attempts
  involving various convolution kernels.  Cf:


• 20030519: The new bicubic interpolation algorithm turned out to be too slow when applied to master pictures
  (which can weigh in at several megapixels), rather than working images (slightly above 300 Kpixels).
  MLAffineTransformBicubic takes a hefty 24.56 seconds to process a 2048x1536 photograph on my (admittedly lowly)
  350 MHz G4.  Fortunately, just applying some standard PowerPC optimizations I was able to slim this figure
  down to less than 13s.  Still too much.  We should probably try using a look-up table of precomputed values
  for the cubic weighting function (cwf) used in the innermost loop, which is pretty expensive.
  If that fails, I'll consider spinning calculations off to a separate (preemptive?) thread, so general
  application responsiveness isn't compromised.


  DigitalEmotionPhoto::ComposeCanvas() now takes a new InterpolationType parameter specifying which interpolation
  algorithm should be used when applying the affine transform.  Possible values for InterpolationType include
  kInterpolationNearestNeighbor, kInterpolationBilinear (not currently implemented), kInterpolationBicubic.

  Two new application preference keys, kKeyPreferencesPrintInterpolationType and kKeyPreferencesSlideShowInterpolationType,
  are used to store the preferred interpolation algorithm for printing and slide show projection, respectively.

• 20030520: Meeting in Monterotondo (RM) with Leonardo, Marco and Milco (sp?).

  We performed a number of printing tests, using several different programs and interpolation algorithms,
  on both Macs and PCs.  For some reason, prints produced by Digital Emotion (even using bicubic interpolation)
  aren't as crisp and smooth as those produced by a competing application.  I'm puzzled.

• 20030521: Looking for info on state-of-the-art (better than bicubic) interpolation algorithms.
  Googling around, it looks like two such algorithms are Lanczos, used by IrfanView and QImagePro,
  and the proprietary (and patented) S-Spline.


  An interesting thread in


  A textbook that appears to be a fairly thorough reference on the subject:

		George Wolberg
		IEEE Computer Society Press Monograph
		Wiley / IEEE Press (ISBN: 0-81868-944-7)

• 20030523: Changed DigitalEmotionAlbum::GoToPreviousWorkingSet() so that the current selection isn't touched.
  This change tentatively addresses [entry #4 of 20030512 wish list].
  The selection count wasn't always properly updated after a Find or Find All.  The layout mode now reverts to
  PHOTO after a Find or Find All. [entry #3 of 20030512 wish list]

  In 4/9/16 viewer modes, DigitalEmotionApp::DisplayPhoto() now sets mCurrentOneViewerPhotoID to the
  picture in the top-left corner, so that switching modes keeps that picture and its neighbors visible.
  [entry #6 of 20030512 wish list]

  In order to fix a subtle bug with dates and the Macintosh clock control, I re-engineered the MLDate
  class to use a 64-bit mMacSeconds field instead of the old 32-bit clock.  The problem was that the
  kControlClockTypeHourMinute variant of the clock control (which only has hour and minute fields, but
  not year/month/day fields) would not fill in all the fields of the LongDateRec structure passed to it.
  MLMacClockControl::GetTime() now clears the LongDateRec structure before calling GetControlData(),
  but that yields a date which cannot be represented by the old 32-bit clock.
  This change addressed [entry #5 of 20030512 wish list].

  In one-photo viewer mode, DigitalEmotionApp::DisplayPhoto() will now longer adapt the bounds of the
  selection button to the opaque region of the picture.  This leaves more room to the button, and makes
  it much easier to click. [entry #8 of 20030512 wish list].

  The tooltip that appears when you point the photo label in one-photo viewer mode now displays "No date tag found"
  if the corresponding picture file doesn't include a valid date Exif tag.

  Added an "Interpolation Method" popup menu to the Print Options window that allows the user to specify
  the interpolation type on a print-by-print basis.

• 20030524: In all the modal dialog windows, the return/enter and esc keys are now properly mapped to the
  default and cancel button, respectively.  I had to make some changes to the low-level plumbing of the
  framework to make this happen; in particular, the MLCard class now has two new MLAction * members
  (mDefaultAction and mCancelAction).

• 20030526: Made DigitalEmotionAlbum::Load() more robust against missing spool files.
  Started work on color balance controls.

• 20030527: Implemented color balance controls, as three new sliders in the Color Palette: Red/Cyan,
  Green/Magenta, Blue/Yellow.  The default balance is 0.0 -- the allowed range is -.25 to +.25.
  Applying a red/cyan color balance correction amounts to concatenating the following matrix to
  the product of the existing contrast/brightness, saturation and hue matrices:

			1.0				0.0					0.0
		[	0.0				1.0					0.0		]
			0.0				0.0					1.0
		  balance		-balance/2.0		-balance/2.0

  i.e., moving every point in RGB space along the plane R+G+B = k (where k is constant), either
  closer to (balance > 0) or father away from (balance < 0) the red axis.
  Since the three new sliders couldn't fit neatly in the existing window, I made the Color Palette
  "tabbed": you can switch between two cards (kCardColorPalette and kCardColorBalance) using a tab
  control at the top of the window.  A new MLMacTabControl class is a thin wrapper around the standard
  Macintosh tab control.

• 20030601: Started integrating the IJG (Independent JPEG Group) library into the project.  This library
  will provide an alternative codepath for compressing/decompressing JPEG files, which is currently done
  using QuickTime graphics exporter/importer components.

  Two main reasons for this choice:
  	1.	We have found the progress callback provided by the QuickTime JPEG components to be unreliable
  		(the callback is seldom, if ever, invoked) and this negatively affects the responsiveness of
  		the application while images are being imported by the DigitalEmotionImporter thread.
  		We are confident that the IJG library will offer finer control on the (de-)compression process.

	2.	We don't want the forthcoming Windows version of Digital Emotion to depend on QuickTime.

  One problem with this choice is that the IJG library does not offer direct support for Exif tags found
  in pictures created by just about all commercial digital cameras, and we need to parse at least one Exif
  tag: the one with the timestamp of the picture.  There appears to be one open-source project which could
  possibly be integrated into Digital Emotion for the purpose of parsing Exif tags: "libexif".