Mimsy Were the Borogoves

Hacks: Articles about programming in Python, Perl, Swift, BASIC, and whatever else I happen to feel like hacking at.

Image dimensions and orientation in Mac OS X Python

Jerry Stratton, October 20, 2011

Often you’ll want to get the height and width of attachments that are images. I was able to find two command-line programs on Mac OS X that will provide an image’s dimensions: /usr/bin/sips and /usr/bin/mdls. “SIPS” stands for Scriptable Image Processing System, where “mdls” is for listing metadata.

In my tests, sips was more accurate: the metadata appears to not get filled out immediately. It’s fast enough for humans, but not fast enough for scripts. The height and width metadata were empty when using mdls in the script, whereas sips was always able to provide it.

Add these two methods to the Attachment class:

[toggle code]

  • def dimensions(self):
    • if not self.isImageSaved():
      • return None, None
    • #get dimensions from sips
    • dimensionArgs = ['/usr/bin/sips', '-g', 'pixelHeight', '-g', 'pixelWidth', self.path]
    • sips = subprocess.Popen(dimensionArgs, stdout=subprocess.PIPE)
    • dimensions = sips.stdout.read()
    • dimensions = re.findall(r'pixel(Height|Width): ([0-9]+)', dimensions)
    • if len(dimensions) == 2:
      • label, height = dimensions[0]
      • label, width = dimensions[1]
      • height = int(height)
      • width = int(width)
      • if height and width:
        • return width, height
    • return False, False
  • def isImageSaved(self):
    • if not self.path:
      • print 'Attachment', self.file, 'has not yet been saved.'
      • return False
    • if self.fileKind != 'image':
      • print 'Attachment', self.path, 'is not an image:', self.fileKind
      • return False
    • return True

The “isImageSaved” method just checks to make sure that the attachment has been saved and it is in fact an image. The “dimensions” method returns width, height if it can find it, using “/usr/bin/sips -g pixelHeight -g pixelWidth image path”. It uses a regular expression to parse the response from sips. Pretty basic stuff.

At the bottom of the script, in the “for message in unPublishedMessages:” loop, add an attachments loop:

[toggle code]

  • if post.replyTo:
    • print 'In reply to', post.replyTo
  • if post.attachments:
    • print 'Attachments:'
    • for attachment in post.attachments:
      • print "\t", attachment.path, attachment.dimensions()
  • print

Not too bad. You could use this to do further things with the attachments, depending on what kind of files they are, how big they are, etc.

Image orientation

Things were going great until I sent a couple of photos from the iPad. Viewing them in a web browser, about half of the photos were sideways! They displayed “correctly” in Preview, and the Finder icons were also correct, but both Safari and Firefox displayed them sideways in the web page my script generated.

Looking at the more info:general pane in Preview’s inspector, the offending images had an “Orientation” field and the non-offending images did not. The value of the Orientation field was “6 (Rotated 90° CCW)”. Looking around, I could find other images with an Orientation field of “1 (Normal)”.

Unfortunately, sips didn’t list any orientation data; it listed the width and height as the browser displayed the images, not as Preview displayed the images.

However, there was a likely candidate in mdls: for the ones rotated 90 degrees, it listed a kMDItemOrientation or 1, and for the ones with “normal” orientation, it listed a kMDItemOrientation of 0. According to Apple’s MDItem reference, zero means landscape and one means portrait. Sometimes it will be set and the image will already be rotated correctly to it; other times it will be set and the image is not rotated correctly.

I’m guessing that I can check by comparing the height and width of the image to the orientation; if it’s portrait and the width is greater than the height, it needs to be rotated1. And sips can rotate images, so that part is easy.

[toggle code]

  • import re, subprocess, time
  • class Attachment(object):
    • #sometimes images come through with their orientation incorrect
    • def orient(self):
      • if not self.isImageSaved():
        • return None
      • #verify orientation
      • width, height = self.dimensions()
      • #need time for the metadata to get attached to the file
      • time.sleep(1)
      • orientationArgs = ['/usr/bin/mdls', '-name', 'kMDItemOrientation', self.path]
      • mdls = subprocess.Popen(orientationArgs, stdout=subprocess.PIPE)
      • mdlsOutput = mdls.stdout.read()
      • orientation = re.findall(r'kMDItemOrientation[ \t+]=[ \t]+([0-9]+)', mdlsOutput)
      • if len(orientation) == 1:
        • orientation = int(orientation[0])
        • #is it supposed to be portrait but is landscape instead?
        • if orientation == 1 and width > height:
          • rotation = 90
        • #is it supposed to be landscape but is portrait instead?
        • elif orientation == 0 and height > width:
          • #note: this is a guess
          • rotation = -90
        • else:
          • return 0
        • rotationArgs = ['/usr/bin/sips', '--rotate', str(rotation), self.path]
        • subprocess.Popen(rotationArgs, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
        • #need time for the new height and width to be applied?
        • #without sleeping a second, self.dimensions sometimes returns reversed height/width
        • time.sleep(1)
        • return rotation

Again, if the attachment hasn’t been saved or isn’t an image, it returns. Otherwise, it gets the height and width so that it can verify that the image is landscape and/or portrait.

It uses /usr/bin/mdls to get the kMDItemOrientation, but it has to wait: I’ve found that if I don’t have it sleep for a second, kMDItemOrientation will be empty. If the orientation doesn’t match the height and width, it rotates the image by 90 degrees using sips.

Then, it sleeps for another second, because about one in three times an immediate call to attachment.dimensions() after a rotation will result in the old values for height and width.

Modify the attachment loop to call the “orient” method:

[toggle code]

  • for attachment in post.attachments:
    • attachment.orient()
    • print "\t", attachment.path, attachment.dimensions()

I don’t call orient automatically in the attachment object2, because you’ll only want to re-orient an attached image if your purpose for this is online display or non-GUI display. If you just want to look at them in Preview, don’t re-orient them. I couldn’t find any way to modify kMDItemOrientation, which means that after sips rotates the image, the value of kMDItemOrientation is incorrect—and while web browsers will now display the image correctly, Preview will display it sideways.

Only JPEG images need rotating?

It doesn’t appear that PNG images ever need rotating; so rather than incur the one-second delay, I only check JPEG files for mismatched orientation. It makes sense that JPEG images will need virtual rotation where PNG images don’t: PNG is a non-lossy format, but every time you save a JPEG the image’s quality deteriorates. Rotating a JPEG image means decoding the JPEG, rotating the resulting pixels, and then re-encoding the JPEG; at the re-encoding stage, image quality will drop. Rotate an image enough and you will see the effects.

My guess is that Mac OS X/iOS uses the orientation metadata field to avoid this deterioration. Thus, there’s no reason for this complexity for PNGs: it just goes ahead and actually rotates the image. If I’m wrong, and you run across non-JPEG images that need rotation, just remove the “if self.fileFormat in ['jpeg', 'jpg']:” and de-indent that section.

No, PNGs also need rotating on occasion. Not sure what I was seeing there. I’ve removed the limitation to only rotate jpeg images.

In response to Using appscript with Apple Mail to get emails and attachments: Python and appscript can be used to get email messages out of Apple’s OS X Mail client, and even to get and save attachments.

  1. I’m also guessing that if it is landscape and the height is greater than the width, it needs to be rotated -90 degrees. But I haven’t seen any like that, so it’s just a wild guess.

  2. If I were to always re-orient images, I’d probably call the orient method right after saving the image in the “save” method.