Dev.to WebDev ๐Ÿ›  Dev ๐Ÿ‘ 0 ๐Ÿ“– 4 min read

Why your photo-to-PDF pages come out in the wrong order (and how to sort them in the browser)

If you've built an "images to PDF" feature, you've probably had this bug report: "I picked page 1, 2, 3, 4 and the PDF came out 3, 1, 4, 2." Nothing is wrong with your PDF code. The problem is the order the files arrive

If you've built an "images to PDF" feature, you've probably had this bug report: "I picked page 1, 2, 3, 4 and the PDF came out 3, 1, 4, 2."

Nothing is wrong with your PDF code. The problem is the order the files arrive in, and on phones that order is surprisingly unreliable. Here is what actually happens and a small sorting strategy that fixes most cases without uploading anything to a server.

Where the order comes from

When a user picks several files with <input type="file" multiple>, the FileList you get back is in whatever order the OS picker hands over. That depends on the platform:

  • Android (Chrome, Photos picker): usually the order the user tapped, but some gallery apps return them by date added instead.
  • iOS Safari: the order of selection most of the time, but images converted from HEIC on the fly can shuffle.
  • Desktop file dialogs: often alphabetical by file name, regardless of click order.

So "the order the user selected" is not something you can count on. You need a deterministic fallback, and ideally a way for the user to fix it.

Signal 1: the file name

Camera apps name files with a timestamp, which sorts nicely if you compare numbers rather than strings:

const collator = new Intl.Collator(undefined, { numeric: true, sensitivity: "base" });
files.sort((a, b) => collator.compare(a.name, b.name));

The numeric: true option matters. A plain string sort puts page10.jpg before page2.jpg; a numeric collation does not.

The catch: images that went through WhatsApp, Telegram or a screenshot tool get renamed (IMG-20261008-WA0004.jpg, image.jpg, 1000012345.jpg), so the name may only reflect when the file was received, not when the photo was taken.

Signal 2: lastModified

File.lastModified looks tempting, but on mobile it is usually the time the file was written to the device or copied into the picker's cache. Ten photos received in one chat often share the same second. Use it as a tie-breaker, not as the main key.

Signal 3: EXIF DateTimeOriginal

For real camera photos, the best signal is inside the JPEG itself. EXIF tag 0x9003 (DateTimeOriginal) records when the shutter fired. You can read it from the first few kilobytes of the file without any library:

async function exifDate(file) {
  const buf = await file.slice(0, 128 * 1024).arrayBuffer();
  const view = new DataView(buf);
  if (view.getUint16(0) !== 0xffd8) return null; // not a JPEG

  let offset = 2;
  while (offset < view.byteLength - 4) {
    const marker = view.getUint16(offset);
    const size = view.getUint16(offset + 2);
    if (marker === 0xffe1) return readApp1(view, offset + 4);
    offset += 2 + size;
  }
  return null;
}

function readApp1(view, start) {
  // "Exif\0\0" header, then a TIFF header
  if (view.getUint32(start) !== 0x45786966) return null;
  const tiff = start + 6;
  const little = view.getUint16(tiff) === 0x4949;
  const u16 = (o) => view.getUint16(o, little);
  const u32 = (o) => view.getUint32(o, little);

  const findTag = (ifd, tag) => {
    const count = u16(ifd);
    for (let i = 0; i < count; i++) {
      const entry = ifd + 2 + i * 12;
      if (u16(entry) === tag) return entry;
    }
    return null;
  };

  const ifd0 = tiff + u32(tiff + 4);
  const exifPtr = findTag(ifd0, 0x8769); // pointer to Exif sub-IFD
  if (!exifPtr) return null;
  const exifIfd = tiff + u32(exifPtr + 8);
  const dt = findTag(exifIfd, 0x9003);
  if (!dt) return null;

  const strOffset = tiff + u32(dt + 8);
  let s = "";
  for (let i = 0; i < 19; i++) s += String.fromCharCode(view.getUint8(strOffset + i));
  // "2026:10:08 14:03:22" -> sortable number
  return Number(s.replace(/\D/g, ""));
}

Messaging apps often strip EXIF, so this returns null for many forwarded images. That's fine: it just means falling back to the next signal.

Putting it together

Sort by the strongest signal each pair of files has in common:

async function orderPages(fileList) {
  const items = await Promise.all(
    [...fileList].map(async (file, index) => ({
      file,
      index,                       // original picker order
      shot: await exifDate(file),  // may be null
    }))
  );

  items.sort((a, b) => {
    if (a.shot && b.shot && a.shot !== b.shot) return a.shot - b.shot;
    const byName = collator.compare(a.file.name, b.file.name);
    if (byName !== 0) return byName;
    if (a.file.lastModified !== b.file.lastModified) {
      return a.file.lastModified - b.file.lastModified;
    }
    return a.index - b.index;
  });

  return items.map((i) => i.file);
}

Strictly speaking, mixing keys like this isn't a perfect total order when some files have EXIF and others don't. In practice, a batch tends to be all camera photos or all forwarded images, and the result is far better than trusting the picker.

Always let people reorder

No heuristic will be right every time, especially for documents like an ID card where the user wants "front, then back" no matter when each photo was taken. Show thumbnails and make them draggable before generating the PDF. Pointer events plus a simple swap is enough; you don't need a drag-and-drop library for a list of ten images.

Two small details that save support messages:

  1. Number the thumbnails ("Page 1", "Page 2") so the intended order is obvious.
  2. Keep the user's manual order if they add more images later, and append the new ones at the end instead of re-sorting everything.

Keep it on the device

All of this runs on the File objects in memory, so there is no reason to upload anything just to figure out page order. That matters when the photos are ID documents or bank statements.

If you want to see the end result, JPG to PDF Mobile is a free converter that lets you reorder pages and builds the PDF entirely in the browser, so the photos never leave the phone.

If you've hit other ordering quirks on specific phones or gallery apps, I'd like to hear about them in the comments.

๐Ÿ“ฐ Read the original article on Dev.to WebDev

Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes โ€” full credit and traffic to the original publisher.