How Images to XLSX is built
Images to XLSX builds an Excel index of images — a thumbnail in column A, file name, size, and basic info alongside it, one row per image. This post is about the one setting that determines whether that thumbnail actually behaves like a normal Excel picture, and how a wrong assumption about it was caught before shipping.
Tech used
ExcelJS, and the OOXML anchor that places a picture on a sheet
Building the workbook uses ExcelJS 4.4 (MIT), loaded via a dynamic import() so its ~940 KB stays out of the page’s initial bundle. Placing an image on a worksheet in OOXML isn’t a matter of pixel coordinates — it’s an anchor, a drawing-XML element that ties the image’s corners to specific cell positions rather than to a fixed pixel location. worksheet.addImage(imageId, { tl, br }) takes a top-left and bottom-right cell reference (tl/br), each expressible as fractional cell coordinates, which is what lets a thumbnail sit inside a specific cell rather than floating at an absolute page position.
Rendering thumbnails with the Canvas API, not a library
Each source image is decoded with createImageBitmap() (falling back to an <img> element where that API is unavailable or fails), drawn onto a fixed-size OffscreenCanvas, and re-encoded as JPEG via convertToBlob(). This entire pipeline is browser built-ins — no image-processing library is added; the aspect ratio is preserved by computing a contained size (scale to fit, never upscale past 1:1) before drawing, so a thumbnail never distorts regardless of the source image’s proportions.
Implementation & operational notes
A one-word attribute decided whether the whole feature actually worked, and the library’s own default silently picked the wrong one. OOXML’s anchor element has an editAs attribute with three possible values: twoCell (move and resize with the cells it’s anchored to), oneCell (move but never resize), and absolute (neither). The tool’s core requirement — a thumbnail that grows or shrinks when column A’s width or a row’s height changes — needs twoCell. Reading ExcelJS’s writer source directly (two-cell-anchor-xform.js) turned up this line:
xmlStream.openNode(this.tag, {editAs: model.range.editAs || 'oneCell'});
Whenever a caller doesn’t specify editAs, ExcelJS writes oneCell into the XML — silently opting into “move but don’t resize,” the opposite of what a caller relying on the library’s own default would reasonably expect from the OOXML spec (which defaults an absent attribute to twoCell; ExcelJS never leaves it absent). This was verified concretely, not inferred: writing a real .xlsx with editAs unset, unzipping it, and reading xl/drawings/drawing1.xml directly showed editAs="oneCell" in the output. The fix is one line — worksheet.addImage(imageId, { tl, br, editAs: 'twoCell' }) — but it only surfaces by reading a library’s actual behavior, not its documentation or its type signature; the same unit test also reloads the generated workbook and reads back worksheet.getImages()[0].range.editAs to keep that regression from resurfacing silently.
A link relying on color alone to distinguish it from surrounding text is an accessibility bug, and it’s checked automatically. The result screen includes an inline link to Read EXIF for anyone who wants deeper photo metadata than the four columns this tool surfaces. axe-core’s automated accessibility check (run as part of this fleet’s e2e suite) flagged that link for relying on color alone with no underline to separate it from the surrounding sentence — a real WCAG 1.4.1 violation, not a style nitpick, since a user who can’t perceive the color difference would have no way to tell text and link apart. The fix — an explicit underline on that one inline link — is a one-line CSS change, but it only gets caught because the check runs automatically on every build rather than depending on someone remembering to look for it.
The four columns are a deliberately small, fixed set — not an attempt at general-purpose metadata. Thumbnail, file name, size, and dimensions/format/modified-date cover what’s useful in an at-a-glance index. Detailed EXIF (camera settings, GPS coordinates) is a different, larger feature the tool doesn’t attempt — the result screen links to Read EXIF for that instead of half-implementing it here.
Try it / source
- Tool: Images to XLSX
- Source: github.com/GeppettoAndRomero/images-to-xlsx