Getting photos off a 2007 iPod without iTunes

Code structure & architectureNov 3, 2026

My father-in-law found his old iPod in a drawer. It dates from around 2007, and it had photos on it that nobody had looked at for well over a decade (he hadn’t realised it could hold photos at all). He wanted them in Google Photos, which should have been a ten minute job.

It wasn’t, because iTunes was what used to talk to these things and there is no iTunes on an Apple silicon Mac. I couldn’t find anything else that would pull photos off an iPod that old without a fight, so I built a script with an agentic AI doing the legwork (more on that below). It finds the connected iPod, reads the photos off it and saves them as PNGs. On his it pulled off over 900 photos, more than 500MB between them (the smaller copies the iPod kept, not the originals). It’s one Python file that needs nothing installed, and the source and a page with the steps are linked at the bottom.

This post is about how it works, because the surprising part is that an iPod doesn’t keep your photos as photos.

Getting at the iPod

Put the iPod in disk mode (there’s a button combination, it’s on the project page) and it turns up as an ordinary drive. The script looks through /Volumes for a volume with a Photos or iPod_Control folder. One match and it carries on. Several and it prints a command for each, so you pick. That detection is macOS only, so on anything else you hand it the path yourself.

From there it makes up to three passes. The last one only runs if the second finds nothing.

Pass one: the originals

If iTunes was told to include full-resolution photos when it synced, the original files are sitting in Photos/Full Resolution. The script copies them across untouched. One small catch is that macOS leaves ._ stubs on a drive that carry a .jpg extension but aren’t photos, so those get skipped.

If they aren’t there, the best you can get is the small copy the iPod made for its own screen and for TV-out. On his iPod they weren’t. That copy is up to 720×480, which is a lot less than the camera took. Nothing can fix that. The pixels aren’t on the device.

What an .ithmb file is

The small copies live in .ithmb files in Photos/Thumbs, and they are not an image format. There’s no header and no compression. It’s raw pixels, frame after frame, each frame in a fixed-size slot. The F1067 in F1067_1.ithmb is a number that says how big the frames are and how the pixels are packed, and it’s the only clue you get.

The script keeps a table of 21 of these formats. Between them they come down to four pixel packings:

  • RGB565, in both byte orders: 16 bits a pixel, 5 for red, 6 for green and 5 for blue.
  • BGR555: 15 bits a pixel, the colours in the other order.
  • Planar YCbCr 4:2:0 (format 1067): all the brightness values first, then colour stored once for every 2×2 block of pixels.
  • Interlaced UYVY 4:2:2 (format 1019): the TV-out one. The first half of the frame holds the even rows and the second half the odd rows, the way an old telly drew them, so you decode two fields and weave them back together.

The RGB565 decode is the simplest of the lot. There are only 65,536 possible 16-bit values, so the script works out the red, green and blue for every one of them once at start-up and decoding a pixel becomes a lookup:

ipod_photo_extractor.py
for _val in range(65536):
    _r5 = (_val >> 11) & 0x1F
    _g6 = (_val >> 5) & 0x3F
    _b5 = _val & 0x1F
    _RGB565_R[_val] = (_r5 << 3) | (_r5 >> 2)
    _RGB565_G[_val] = (_g6 << 2) | (_g6 >> 4)
    _RGB565_B[_val] = (_b5 << 3) | (_b5 >> 2)

The shifts stretch a 5 bit value to 8 bits. The >> 2 bit on the end is what stops full red coming out as 248 instead of 255.

Pass two: the Photo Database

Knowing how to decode a frame doesn’t tell you which frame is which photo. That’s the job of the Photo Database file, a binary format made of blocks with four-letter tags. mhfd is the file header, an mhii is one photo, and inside each one is an mhni for every size of thumbnail the iPod stored. An mhni gives you the format ID, the offset of the frame inside the .ithmb file, and where the picture sits within the frame. A child block, mhod, carries the file name as UTF-16 with a Mac-style path, :Thumbs:F1067_1.ithmb.

I don’t walk that tree properly. The script scans the file for the mhii tag, then for mhni tags inside it, and reads the fields at fixed offsets. It’s less elegant than a real parser, but I only needed a handful of fields and it doesn’t care what lies between them.

Two quirks to know about:

  • The database can be little or big endian. A big-endian one starts with dfhm, which is mhfd backwards, so the script checks the first four bytes and flips every read.
  • On an iPod Classic the name block’s type is 0x02000003 rather than 3, so the script only compares the bottom 16 bits.

Each photo has several thumbnails at different sizes, so the script sorts them by pixel area and takes the biggest. A 720×480 beats a 320×240 every time.

Cropping the black bars

Frames are a fixed size whatever the photo’s shape, so a photo that doesn’t match gets black bars. The database stores how much padding to ignore and how big the real picture is, which means the crop rectangle is the padding plus the size. There’s a test for that one, and the test data is made up, so see the caveats below.

If you’d rather keep the bars, --no-crop leaves them in.

Writing PNGs with nothing installed

The usual answer for writing images in Python is Pillow. That’s a dependency, and the person running this might be pasting two lines into Terminal for the first time, so I didn’t want one. A PNG turns out to be a fixed 8 byte signature followed by three chunks (IHDR, IDAT, IEND), each with its length, a tag, its data and a CRC32. struct and zlib are both in the standard library, and the writer is about 20 lines. I compress at level 1, so the files are a little bigger than they could be and the whole run is quick.

Pass three: carving

If there’s no database, or it yields nothing, the script falls back to carving. It slices every .ithmb file into frame-sized slots, using the slot size from the format table, and decodes each one. You get every frame there is, with no names and no link back to which photo it was. It also takes the album art files, because it can’t tell them apart from photos.

How long it took

About 30 minutes, built with an agentic AI. Doing it all myself, reading up on .ithmb files and YCbCr and the layout of the Photo Database, would have taken a few weeks, and I’d likely just not have done it. His photos would still be in the drawer.

It’s also why the next section exists. I got to a working script quickly, and I’d rather say which parts nobody has tried on real hardware than let the speed suggest otherwise.

What it gets wrong

I’d rather tell you than have you find out.

  • A photo that is all one colour is skipped. The script samples about 1,000 pixels, and if they are all the same it assumes the slot is empty. So a genuinely all-black or all-white photo is lost.
  • Not every format has been checked on a real device. The table comes from the format definitions, and the tests build fake .ithmb files and a fake Photo Database, so no photos are in the repo. There are six tests. If a photo comes out with odd colours or shifted rows, open an issue with your iPod model and the F#### names from Photos/Thumbs.

None of that bit on his iPod. Every photo that came out was perfect.

The script is free and MIT licensed. The source is on GitHub, and ipodphotos.theflyingcoders.dev has the steps written for someone who has never opened Terminal. There’s a short write-up in Work too. If you’ve got an iPod in a drawer and it works, I’d like to hear which model it was.

Taggedpythonipodbinary formatsopen sourceai-assisted

Working through this on a live service? We do this kind of work as a fixed-scope engagement.

Backend platform delivery