JPEG decoder crashes on TIFF files with unpadded edge tiles
thank you for the project!
this was detected by Opus when vibecoding an app using your library. hope for quick fix;
Bug
readRasters / readRGB throws RangeError: Offset is outside the bounds of the DataView when decoding JPEG-compressed tiled TIFFs whose edge tiles are not zero-padded to the full tile dimensions.
Reproduction
Any JPEG-compressed (compression=7) tiled TIFF where ImageWidth is not a multiple of TileWidth or ImageLength is not a multiple of TileLength, and the TIFF writer encoded edge tiles at their actual pixel dimensions rather than padding them to the full tile size.
Example: a 3744×5616 image with 256×256 tiles. The last column of tiles covers 160 pixels (3744 mod 256 = 160). The JPEG data inside those tiles has SOF dimensions 160×256 instead of 256×256.
const tiff = await GeoTIFF.fromArrayBuffer(buffer);
const image = await tiff.getImage(0);
await image.readRGB({ interleave: true }); // RangeError
Root cause
In src/geotiffimage.js, _readRaster computes pixel offsets into the decoded tile buffer using tileWidth as the row stride (line ~561):
const pixelOffset = ((y * tileWidth) + x) * bytesPerPixel;
The JPEG decoder in src/compression/jpeg.js returns a buffer whose dimensions match the JPEG SOF header (frame.samplesPerLine × frame.scanLines). For edge tiles this is smaller than tileWidth × tileHeight, so the offset goes out of bounds.
getBlockHeight(y) already handles the last strip row for stripped images, but for tiled images it always returns tileHeight (line ~291). getBlockWidth() always returns tileWidth. Neither accounts for the actual decoded buffer size.
TIFF spec context
TIFF 6.0 §15 says edge tiles "are padded to tile boundaries", but many real-world writers (including some that produce BigTIFF) emit unpadded edge tiles. libtiff, GDAL, and other readers handle both cases.
Proposed fix
Pad the JPEG decoder output to the expected tile dimensions in JpegDecoder.decodeBlock(). The decoder already has access to this.parameters.tileWidth and this.parameters.tileHeight, and the actual JPEG dimensions are available from the parsed frame.
// src/compression/jpeg.js — JpegDecoder.decodeBlock()
decodeBlock(buffer) {
this.reader.resetFrames();
this.reader.parse(new Uint8Array(buffer));
const result = this.reader.getResult();
const { tileWidth, tileHeight } = this.parameters;
const frame = this.reader.frames[0];
if (frame) {
const jpegW = frame.samplesPerLine;
const jpegH = frame.scanLines;
const numComponents = frame.componentsOrder.length;
if (jpegW !== tileWidth || jpegH !== tileHeight) {
const padded = new Uint8Array(tileWidth * tileHeight * numComponents);
for (let y = 0; y < jpegH; y++) {
const srcOff = y * jpegW * numComponents;
const dstOff = y * tileWidth * numComponents;
padded.set(
result.subarray(srcOff, srcOff + jpegW * numComponents),
dstOff,
);
}
return padded.buffer;
}
}
return result.buffer;
}
This copies each row of the decoded JPEG into a zero-padded buffer with the correct stride. The extra pixels are zero-filled and never read by _readRaster because xmax/ymax already clamp iteration to the image extent.
An alternative fix would be in _readRaster itself — derive the actual buffer stride from buffer.byteLength rather than assuming tileWidth. The decoder-side fix is simpler and more contained.
Environment
- geotiff.js v3.0.5
- Browser (UMD bundle)
- BigTIFF, JPEG compression, YCbCr photometric, 256×256 tiles, image dimensions not a multiple of tile size
JPEG decoder crashes on TIFF files with unpadded edge tiles
thank you for the project!
this was detected by Opus when vibecoding an app using your library. hope for quick fix;
Bug
readRasters/readRGBthrowsRangeError: Offset is outside the bounds of the DataViewwhen decoding JPEG-compressed tiled TIFFs whose edge tiles are not zero-padded to the full tile dimensions.Reproduction
Any JPEG-compressed (compression=7) tiled TIFF where
ImageWidthis not a multiple ofTileWidthorImageLengthis not a multiple ofTileLength, and the TIFF writer encoded edge tiles at their actual pixel dimensions rather than padding them to the full tile size.Example: a 3744×5616 image with 256×256 tiles. The last column of tiles covers 160 pixels (3744 mod 256 = 160). The JPEG data inside those tiles has SOF dimensions 160×256 instead of 256×256.
Root cause
In
src/geotiffimage.js,_readRastercomputes pixel offsets into the decoded tile buffer usingtileWidthas the row stride (line ~561):The JPEG decoder in
src/compression/jpeg.jsreturns a buffer whose dimensions match the JPEG SOF header (frame.samplesPerLine × frame.scanLines). For edge tiles this is smaller thantileWidth × tileHeight, so the offset goes out of bounds.getBlockHeight(y)already handles the last strip row for stripped images, but for tiled images it always returnstileHeight(line ~291).getBlockWidth()always returnstileWidth. Neither accounts for the actual decoded buffer size.TIFF spec context
TIFF 6.0 §15 says edge tiles "are padded to tile boundaries", but many real-world writers (including some that produce BigTIFF) emit unpadded edge tiles. libtiff, GDAL, and other readers handle both cases.
Proposed fix
Pad the JPEG decoder output to the expected tile dimensions in
JpegDecoder.decodeBlock(). The decoder already has access tothis.parameters.tileWidthandthis.parameters.tileHeight, and the actual JPEG dimensions are available from the parsed frame.This copies each row of the decoded JPEG into a zero-padded buffer with the correct stride. The extra pixels are zero-filled and never read by
_readRasterbecausexmax/ymaxalready clamp iteration to the image extent.An alternative fix would be in
_readRasteritself — derive the actual buffer stride frombuffer.byteLengthrather than assumingtileWidth. The decoder-side fix is simpler and more contained.Environment