Image dimensions and orientation in Mac OS X Python
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
-
if not self.isImageSaved():
-
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
-
if not self.path:
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()
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
-
if not self.isImageSaved():
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.
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.
↑If I were to always re-orient images, I’d probably call the orient method right after saving the image in the “save” method.
↑
- MDItem Reference
- “MDItem is a CF-compliant object that represents a file and the metadata associated with the file.”
- One great iPad music app, and one good iPad music app
- I don’t remember how I ran across Guitar World’s Lick of the Day app. I might have been looking around for MusicNotes.com’s app, which I ran across while looking for sheet music of America, the Beautiful. There was surprisingly little good, simple versions I could use for guitar, for free. MusicNotes.com put the full first page of their version online, and it worked, so after a few weeks of internal whining about the lack of a good free version,…
More images
- Caption this! Add captions to image files
- Need a quick caption for an image? This command-line script uses Swift to tack a caption above, below, or right on top of, any image macOS understands.
- Calculate poster pixel sizes from an existing image file
- This script takes a height and a width in whatever units you wish, an existing set of pixel dimensions for an image file, and calculates which pixel dimension should be cropped to match the poster size.
More JPEG
- Photo Resize, Rotate, Flip & Compress
- This is a simple app that fills a surprising gap in Photo apps: it allows resizing and compressing images with automatic recalculation of file size.