@loaders.gl/images
Version:
Framework-independent loaders and writers for images (PNG, JPG, ...)
153 lines (152 loc) • 5.38 kB
JavaScript
// Attributions
// * Based on binary-gltf-utils under MIT license: Copyright (c) 2016-17 Karl Cheng
import { getISOBMFFMediaType } from "./parse-isobmff-binary.js";
const BIG_ENDIAN = false;
const LITTLE_ENDIAN = true;
/**
* Extracts `{mimeType, width and height}` from a memory buffer containing a known image format
* Currently supports `image/png`, `image/jpeg`, `image/bmp` and `image/gif`.
* @param binaryData: DataView | ArrayBuffer image file memory to parse
* @returns metadata or null if memory is not a valid image file format layout.
*/
export function getBinaryImageMetadata(binaryData) {
const dataView = toDataView(binaryData);
return (getPngMetadata(dataView) ||
getJpegMetadata(dataView) ||
getGifMetadata(dataView) ||
getBmpMetadata(dataView) ||
getISOBMFFMetadata(dataView));
}
// ISOBMFF
function getISOBMFFMetadata(binaryData) {
const buffer = new Uint8Array(binaryData instanceof DataView ? binaryData.buffer : binaryData);
const mediaType = getISOBMFFMediaType(buffer);
if (!mediaType) {
return null;
}
return {
mimeType: mediaType.mimeType,
// TODO - decode width and height
width: 0,
height: 0
};
}
// PNG
function getPngMetadata(binaryData) {
const dataView = toDataView(binaryData);
// Check file contains the first 4 bytes of the PNG signature.
const isPng = dataView.byteLength >= 24 && dataView.getUint32(0, BIG_ENDIAN) === 0x89504e47;
if (!isPng) {
return null;
}
// Extract size from a binary PNG file
return {
mimeType: 'image/png',
width: dataView.getUint32(16, BIG_ENDIAN),
height: dataView.getUint32(20, BIG_ENDIAN)
};
}
// GIF
// Extract size from a binary GIF file
// TODO: GIF is not this simple
function getGifMetadata(binaryData) {
const dataView = toDataView(binaryData);
// Check first 4 bytes of the GIF signature ("GIF8").
const isGif = dataView.byteLength >= 10 && dataView.getUint32(0, BIG_ENDIAN) === 0x47494638;
if (!isGif) {
return null;
}
// GIF is little endian.
return {
mimeType: 'image/gif',
width: dataView.getUint16(6, LITTLE_ENDIAN),
height: dataView.getUint16(8, LITTLE_ENDIAN)
};
}
// BMP
// TODO: BMP is not this simple
export function getBmpMetadata(binaryData) {
const dataView = toDataView(binaryData);
// Check magic number is valid (first 2 characters should be "BM").
// The mandatory bitmap file header is 14 bytes long.
const isBmp = dataView.byteLength >= 14 &&
dataView.getUint16(0, BIG_ENDIAN) === 0x424d &&
dataView.getUint32(2, LITTLE_ENDIAN) === dataView.byteLength;
if (!isBmp) {
return null;
}
// BMP is little endian.
return {
mimeType: 'image/bmp',
width: dataView.getUint32(18, LITTLE_ENDIAN),
height: dataView.getUint32(22, LITTLE_ENDIAN)
};
}
// JPEG
// Extract width and height from a binary JPEG file
function getJpegMetadata(binaryData) {
const dataView = toDataView(binaryData);
// Check file contains the JPEG "start of image" (SOI) marker
// followed by another marker.
const isJpeg = dataView.byteLength >= 3 &&
dataView.getUint16(0, BIG_ENDIAN) === 0xffd8 &&
dataView.getUint8(2) === 0xff;
if (!isJpeg) {
return null;
}
const { tableMarkers, sofMarkers } = getJpegMarkers();
// Exclude the two byte SOI marker.
let i = 2;
while (i + 9 < dataView.byteLength) {
const marker = dataView.getUint16(i, BIG_ENDIAN);
// The frame that contains the width and height of the JPEG image.
if (sofMarkers.has(marker)) {
return {
mimeType: 'image/jpeg',
height: dataView.getUint16(i + 5, BIG_ENDIAN), // Number of lines
width: dataView.getUint16(i + 7, BIG_ENDIAN) // Number of pixels per line
};
}
// Miscellaneous tables/data preceding the frame header.
if (!tableMarkers.has(marker)) {
return null;
}
// Length includes size of length parameter but not the two byte header.
i += 2;
i += dataView.getUint16(i, BIG_ENDIAN);
}
return null;
}
function getJpegMarkers() {
// Tables/misc header markers.
// DQT, DHT, DAC, DRI, COM, APP_n
const tableMarkers = new Set([0xffdb, 0xffc4, 0xffcc, 0xffdd, 0xfffe]);
for (let i = 0xffe0; i < 0xfff0; ++i) {
tableMarkers.add(i);
}
// SOF markers and DHP marker.
// These markers are after tables/misc data.
const sofMarkers = new Set([
0xffc0, 0xffc1, 0xffc2, 0xffc3, 0xffc5, 0xffc6, 0xffc7, 0xffc9, 0xffca, 0xffcb, 0xffcd, 0xffce,
0xffcf, 0xffde
]);
return { tableMarkers, sofMarkers };
}
// TODO - move into image module?
function toDataView(data) {
if (data instanceof DataView) {
return data;
}
if (ArrayBuffer.isView(data)) {
return new DataView(data.buffer);
}
// TODO: make these functions work for Node.js buffers?
// if (bufferToArrayBuffer) {
// data = bufferToArrayBuffer(data);
// }
// Careful - Node Buffers will look like ArrayBuffers (keep after isBuffer)
if (data instanceof ArrayBuffer) {
return new DataView(data);
}
throw new Error('toDataView');
}