* Declares the piex-wasm Module interface. The Module has many interfaces
* but only declare the parts required for PIEX work.
*/
export interface PiexWasmModule {
calledRun: boolean;
HEAP8: Uint8Array;
_malloc: (size: number) => number;
_free: (size: number) => void;
image: (width: number, height: number) => PiexWasmImageResult;
}
interface VoidCallback {
(): void;
}
* Subset of the Emscripten Module API required for initialization. See
* https://emscripten.org/docs/api_reference/module.html#module.
*/
interface ModuleInitParams {
onAbort: (result: Error|string) => void;
}
* Module defined by 'piex.js.wasm' script upon initialization.
*/
let PiexModule: PiexWasmModule;
declare global {
function createPiexModule(params: ModuleInitParams): Promise<PiexWasmModule>;
}
* Module constructor defined by 'piex.js.wasm' script.
*/
const initPiexModule = globalThis.createPiexModule;
console.info(`[PiexLoader] available [init=${typeof initPiexModule}]`);
* Set true if the Module.onAbort() handler is called.
*/
let piexFailed = false;
const MODULE_SETTINGS = {
* Installs an (Emscripten) Module.onAbort handler. Record that the
* Module has failed in piexFailed and re-throw the error.
*
* @throws {!Error|string}
*/
onAbort: (error: Error|string) => {
piexFailed = true;
throw error;
},
};
let initPiexModulePromise: Promise<void>|null = null;
* Returns a promise that resolves once initialization is complete. PiexModule
* may be undefined before this promise resolves.
*/
function piexModuleInitialized(): Promise<void> {
if (!initPiexModulePromise) {
initPiexModulePromise = new Promise(resolve => {
initPiexModule(MODULE_SETTINGS).then(module => {
PiexModule = module;
console.info(`[PiexLoader] loaded [module=${typeof module}]`);
resolve();
});
});
}
return initPiexModulePromise;
}
* Module failure recovery: if piexFailed is set via onAbort due to OOM in
* the C++ for example, or the Module failed to load or call run, then the
* Module is in a broken, non-functional state.
*
* Loading the entire page is the only reliable way to recover from broken
* Module state. Log the error, and return true to tell caller to initiate
* failure recovery steps.
*
*/
function piexModuleFailed(): boolean {
if (piexFailed || !PiexModule.calledRun) {
console.error('[PiexLoader] piex wasm module failed');
return true;
}
return false;
}
interface PiexPreviewImageData {
thumbnail: ArrayBuffer;
mimeType?: string;
orientation: number;
colorSpace: string;
ifd: string|null;
}
class PiexLoaderResponse {
readonly thumbnail: ArrayBuffer;
readonly mimeType: string;
readonly orientation: number;
readonly colorSpace: string;
readonly ifd: string|null;
* @param data The extracted preview image data.
*/
constructor(data: PiexPreviewImageData) {
this.thumbnail = data.thumbnail;
this.mimeType = data.mimeType || 'image/jpeg';
this.orientation = data.orientation;
this.colorSpace = data.colorSpace;
this.ifd = data.ifd || null;
}
}
const adobeProfile = new Uint8Array([
0xff, 0xe2, 0x02, 0x40, 0x49, 0x43, 0x43, 0x5f, 0x50, 0x52, 0x4f, 0x46,
0x49, 0x4c, 0x45, 0x00, 0x01, 0x01,
0x00, 0x00, 0x02, 0x30, 0x41, 0x44, 0x42, 0x45, 0x02, 0x10, 0x00, 0x00,
0x6d, 0x6e, 0x74, 0x72, 0x52, 0x47, 0x42, 0x20, 0x58, 0x59, 0x5a, 0x20,
0x07, 0xd0, 0x00, 0x08, 0x00, 0x0b, 0x00, 0x13, 0x00, 0x33, 0x00, 0x3b,
0x61, 0x63, 0x73, 0x70, 0x41, 0x50, 0x50, 0x4c, 0x00, 0x00, 0x00, 0x00,
0x6e, 0x6f, 0x6e, 0x65, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf6, 0xd6,
0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xd3, 0x2d, 0x41, 0x44, 0x42, 0x45,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a,
0x63, 0x70, 0x72, 0x74, 0x00, 0x00, 0x00, 0xfc, 0x00, 0x00, 0x00, 0x32,
0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x01, 0x30, 0x00, 0x00, 0x00, 0x6b,
0x77, 0x74, 0x70, 0x74, 0x00, 0x00, 0x01, 0x9c, 0x00, 0x00, 0x00, 0x14,
0x62, 0x6b, 0x70, 0x74, 0x00, 0x00, 0x01, 0xb0, 0x00, 0x00, 0x00, 0x14,
0x72, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0xc4, 0x00, 0x00, 0x00, 0x0e,
0x67, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0xd4, 0x00, 0x00, 0x00, 0x0e,
0x62, 0x54, 0x52, 0x43, 0x00, 0x00, 0x01, 0xe4, 0x00, 0x00, 0x00, 0x0e,
0x72, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x01, 0xf4, 0x00, 0x00, 0x00, 0x14,
0x67, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, 0x08, 0x00, 0x00, 0x00, 0x14,
0x62, 0x58, 0x59, 0x5a, 0x00, 0x00, 0x02, 0x1c, 0x00, 0x00, 0x00, 0x14,
0x74, 0x65, 0x78, 0x74, 0x00, 0x00, 0x00, 0x00, 0x43, 0x6f, 0x70, 0x79,
0x72, 0x69, 0x67, 0x68, 0x74, 0x20, 0x32, 0x30, 0x30, 0x30, 0x20, 0x41,
0x64, 0x6f, 0x62, 0x65, 0x20, 0x53, 0x79, 0x73, 0x74, 0x65, 0x6d, 0x73,
0x20, 0x49, 0x6e, 0x63, 0x6f, 0x72, 0x70, 0x6f, 0x72, 0x61, 0x74, 0x65,
0x64, 0x00, 0x00, 0x00, 0x64, 0x65, 0x73, 0x63, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x11, 0x41, 0x64, 0x6f, 0x62, 0x65, 0x20, 0x52, 0x47,
0x42, 0x20, 0x28, 0x31, 0x39, 0x39, 0x38, 0x29, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0xf3, 0x51, 0x00, 0x01, 0x00, 0x00, 0x00, 0x01, 0x16, 0xcc,
0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x63, 0x75, 0x72, 0x76,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x33, 0x00, 0x00,
0x63, 0x75, 0x72, 0x76, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01,
0x02, 0x33, 0x00, 0x00, 0x63, 0x75, 0x72, 0x76, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x01, 0x02, 0x33, 0x00, 0x00, 0x58, 0x59, 0x5a, 0x20,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9c, 0x18, 0x00, 0x00, 0x4f, 0xa5,
0x00, 0x00, 0x04, 0xfc, 0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x34, 0x8d, 0x00, 0x00, 0xa0, 0x2c, 0x00, 0x00, 0x0f, 0x95,
0x58, 0x59, 0x5a, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x26, 0x31,
0x00, 0x00, 0x10, 0x2f, 0x00, 0x00, 0xbe, 0x9c,
]);
* Piex-wasm extracts the "preview image" from a RAW image. The preview image
* |format| is either 0 (JPEG), or 1 (RGB), and has a JEITA EXIF |colorSpace|
* (sRGB or AdobeRGB1998) and a JEITA EXIF image |orientation|.
*
* An RGB format preview image has both |width| and |height|, but JPEG format
* previews have neither (piex-wasm C++ does not parse/decode JPEG).
*
* The |offset| to, and |length| of, the preview image relative to the source
* data is indicated by those fields. They are positive > 0. Note: the values
* are controlled by a third-party and are untrustworthy (Security).
*/
interface PiexWasmPreviewImageMetadata {
format: number;
colorSpace: string;
orientation: number;
width?: number;
height?: number;
* The offset of the preview image relative to the source data. They are
* positive > 0. Note: ths value is controlled by a third-party and are
* untrustworthy (Security).
*/
offset: number;
* The length of the preview image relative to the source data. They are
* positive > 0. Note: ths value is controlled by a third-party and are
* untrustworthy (Security).
*/
length: number;
}
* The piex-wasm Module.image(<RAW image source>,...) API returns `error`, or
* else the source `preview` and/or `thumbnail` image metadata along with the
* photographic `details` derived from the RAW image EXIF.
*/
interface PiexWasmImageResult {
error: string|null;
preview: PiexWasmPreviewImageMetadata|null;
* The `thumbnail` images are smaller, lower quality, JPEG or RGB format
* images.
*/
thumbnail: PiexWasmPreviewImageMetadata|null;
details: Record<string, any>|null;
}
* Preview Image EXtractor (PIEX).
*/
class ImageBuffer {
private readonly source: Uint8Array;
private readonly length: number;
private memory = 0;
* @param buffer - RAW image source data.
*/
constructor(buffer: ArrayBuffer) {
this.source = new Uint8Array(buffer);
this.length = buffer.byteLength;
}
* Calls Module.image() to process |this.source| and return the result.
*
* @throws {!Error} Memory allocation error.
*/
process(): PiexWasmImageResult {
this.memory = PiexModule._malloc(this.length);
if (!this.memory) {
throw new Error('Image malloc failed: ' + this.length + ' bytes');
}
PiexModule.HEAP8.set(this.source, this.memory);
const result = PiexModule.image(this.memory, this.length);
if (result.error) {
throw new Error(result.error);
}
return result;
}
* Returns the preview image data. If no preview image was found, returns
* the thumbnail image.
*
* @throws {!Error} Data access security error.
*/
preview(result: PiexWasmImageResult): PiexPreviewImageData {
const preview = result.preview;
if (!preview) {
return this.thumbnail_(result);
}
const offset = preview.offset;
const length = preview.length;
if (offset > this.length || (this.length - offset) < length) {
throw new Error('Preview image access failed');
}
const view = new Uint8Array(this.source.buffer, offset, length);
return {
thumbnail: this.createImageDataArray_(view, preview).buffer,
mimeType: 'image/jpeg',
ifd: this.details_(result, preview.orientation),
orientation: preview.orientation,
colorSpace: preview.colorSpace,
};
}
* Returns the thumbnail image. If no thumbnail image was found, returns
* an empty thumbnail image.
*
* @throws {!Error} Data access security error.
*/
private thumbnail_(result: PiexWasmImageResult): PiexPreviewImageData {
const thumbnail = result.thumbnail;
if (!thumbnail) {
return {
thumbnail: new ArrayBuffer(0),
colorSpace: 'sRgb',
orientation: 1,
ifd: null,
};
}
if (thumbnail.format) {
return this.rgb_(result);
}
const offset = thumbnail.offset;
const length = thumbnail.length;
if (offset > this.length || (this.length - offset) < length) {
throw new Error('Thumbnail image access failed');
}
const view = new Uint8Array(this.source.buffer, offset, length);
return {
thumbnail: this.createImageDataArray_(view, thumbnail).buffer,
mimeType: 'image/jpeg',
ifd: this.details_(result, thumbnail.orientation),
orientation: thumbnail.orientation,
colorSpace: thumbnail.colorSpace,
};
}
* Returns the RGB thumbnail. If no RGB thumbnail was found, returns
* an empty thumbnail image.
*
* @throws {!Error} Data access security error.
*/
private rgb_(result: PiexWasmImageResult): PiexPreviewImageData {
const thumbnail = result.thumbnail;
if (!thumbnail || thumbnail.format !== 1) {
return {
thumbnail: new ArrayBuffer(0),
colorSpace: 'sRgb',
orientation: 1,
ifd: null,
};
}
if (!thumbnail.width || !thumbnail.height) {
throw new Error('invalid image width or height');
}
const offset = thumbnail.offset;
const length = thumbnail.length;
if (offset > this.length || (this.length - offset) < length) {
throw new Error('Thumbnail image access failed');
}
const view = new Uint8Array(this.source.buffer, offset, length);
const usesWidthAsHeight = thumbnail.orientation >= 5;
const height = usesWidthAsHeight ? thumbnail.width : thumbnail.height;
const width = usesWidthAsHeight ? thumbnail.height : thumbnail.width;
const rowPad = width & 3;
const rowStride = 3 * width + rowPad;
const pixelDataOffset = 14 + 108;
const fileSize = pixelDataOffset + rowStride * height;
const bitmap = new DataView(new ArrayBuffer(fileSize));
bitmap.setUint8(0, 'B'.charCodeAt(0));
bitmap.setUint8(1, 'M'.charCodeAt(0));
bitmap.setUint32(2, fileSize , true);
bitmap.setUint32(6, 0, true);
bitmap.setUint32(10, pixelDataOffset, true);
bitmap.setUint32(14, 108, true);
bitmap.setInt32(18, width, true);
bitmap.setInt32(22, -height , true);
bitmap.setInt16(26, 1, true);
bitmap.setInt16(28, 24, true);
bitmap.setUint32(30, 0, true);
bitmap.setUint32(34, 0, true);
bitmap.setInt32(38, 0, true);
bitmap.setInt32(42, 0, true);
bitmap.setUint32(46, 0, true);
bitmap.setUint32(50, 0, true);
bitmap.setUint32(54, 0, true);
bitmap.setUint32(58, 0, true);
bitmap.setUint32(62, 0, true);
bitmap.setUint32(66, 0, true);
let rx = 0;
let ry = 0;
let gx = 0;
let gy = 0;
let bx = 0;
let by = 0;
let zz = 0;
let gg = 0;
if (thumbnail.colorSpace !== 'adobeRgb') {
bitmap.setUint8(70, 's'.charCodeAt(0));
bitmap.setUint8(71, 'R'.charCodeAt(0));
bitmap.setUint8(72, 'G'.charCodeAt(0));
bitmap.setUint8(73, 'B'.charCodeAt(0));
} else {
bitmap.setUint32(70, 0);
rx = Math.round(0.6400 * (1 << 30));
ry = Math.round(0.3300 * (1 << 30));
gx = Math.round(0.2100 * (1 << 30));
gy = Math.round(0.7100 * (1 << 30));
bx = Math.round(0.1500 * (1 << 30));
by = Math.round(0.0600 * (1 << 30));
zz = Math.round(1.0000 * (1 << 30));
gg = Math.round(2.1992187 * (1 << 16));
}
bitmap.setUint32(74, rx, true);
bitmap.setUint32(78, ry, true);
bitmap.setUint32(82, zz, true);
bitmap.setUint32(86, gx, true);
bitmap.setUint32(90, gy, true);
bitmap.setUint32(94, zz, true);
bitmap.setUint32(98, bx, true);
bitmap.setUint32(102, by, true);
bitmap.setUint32(106, zz, true);
bitmap.setUint32(110, gg, true);
bitmap.setUint32(114, gg, true);
bitmap.setUint32(118, gg, true);
const h = thumbnail.height - 1;
const w = thumbnail.width - 1;
let dx = 0;
for (let input = 0, y = 0; y <= h; ++y) {
let output = pixelDataOffset;
* Compute affine(a,b,c,d,tx,ty) transform of pixel (x,y)
* { x': a * x + c * y + tx, y': d * y + b * x + ty }
* a,b,c,d in [-1,0,1], to apply the image orientation at
* (0,y) to find the output location of the input row.
* The transform derivative in x is used to calculate the
* relative output location of adjacent input row pixels.
*/
switch (thumbnail.orientation) {
case 1:
output += y * rowStride;
dx = 3;
break;
case 2:
output += y * rowStride + 3 * w;
dx = -3;
break;
case 3:
output += (h - y) * rowStride + 3 * w;
dx = -3;
break;
case 4:
output += (h - y) * rowStride;
dx = 3;
break;
case 5:
output += 3 * y;
dx = rowStride;
break;
case 6:
output += 3 * (h - y);
dx = rowStride;
break;
case 7:
output += w * rowStride + 3 * (h - y);
dx = -rowStride;
break;
case 8:
output += w * rowStride + 3 * y;
dx = -rowStride;
break;
}
for (let x = 0; x <= w; ++x, input += 3, output += dx) {
bitmap.setUint8(output + 0, view[input + 2]!);
bitmap.setUint8(output + 1, view[input + 1]!);
bitmap.setUint8(output + 2, view[input + 0]!);
}
}
if (rowPad) {
let paddingOffset = pixelDataOffset + 3 * width;
for (let y = 0; y < height; ++y) {
let output = paddingOffset;
switch (rowPad) {
case 1:
bitmap.setUint8(output++, 0);
break;
case 2:
bitmap.setUint8(output++, 0);
bitmap.setUint8(output++, 0);
break;
case 3:
bitmap.setUint8(output++, 0);
bitmap.setUint8(output++, 0);
bitmap.setUint8(output++, 0);
break;
}
paddingOffset += rowStride;
}
}
return {
thumbnail: bitmap.buffer,
mimeType: 'image/bmp',
ifd: this.details_(result, thumbnail.orientation),
colorSpace: thumbnail.colorSpace,
orientation: 1,
};
}
* Converts a |view| of the "preview image" to Uint8Array data. Embeds an
* AdobeRGB1998 ICC Color Profile in that data if the preview is JPEG and
* it has 'adodeRgb' color space.
*
*/
private createImageDataArray_(
view: Uint8Array,
preview: PiexWasmPreviewImageMetadata): Uint8Array<ArrayBuffer> {
const jpeg = view.byteLength > 2 && view[0] === 0xff && view[1] === 0xd8;
if (jpeg && preview.colorSpace === 'adobeRgb') {
const data = new Uint8Array(view.byteLength + adobeProfile.byteLength);
data.set(view.subarray(2), 2 + adobeProfile.byteLength);
data.set(adobeProfile, 2);
data.set([0xff, 0xd8], 0);
return data;
}
return new Uint8Array(view);
}
* Returns the RAW image photographic |details| in a JSON-encoded string.
* Only number and string values are retained, and they are formatted for
* presentation to the user.
*
* @param orientation - image EXIF orientation
*/
private details_(result: PiexWasmImageResult, orientation: number): null
|string {
const details = result.details;
if (!details) {
return null;
}
const format: Record<string, string|number> = {};
for (const [key, value] of Object.entries(details)) {
if (typeof value === 'string') {
format[key] = value.replace(/\0+$/, '').trim();
} else if (typeof value === 'number') {
if (!Number.isInteger(value)) {
format[key] = Number(value.toFixed(3).replace(/0+$/, ''));
} else {
format[key] = value;
}
}
}
const usesWidthAsHeight = orientation >= 5;
if (usesWidthAsHeight) {
const width = format['width']!;
format['width'] = format['height']!;
format['height'] = width;
}
return JSON.stringify(format);
}
* Release resources.
*/
close() {
const memory = this.memory;
if (memory) {
PiexModule._free(memory);
this.memory = 0;
}
}
}
* PiexLoader: is a namespace.
*/
export const PiexLoader = {
* Loads a RAW image. Returns the image metadata and the image thumbnail in a
* PiexLoaderResponse.
*
* piexModuleFailed() returns true if the Module is in an unrecoverable error
* state. This is rare, but possible, and the only reliable way to recover is
* to reload the page. Callback |onPiexModuleFailed| is used to indicate that
* the caller should initiate failure recovery steps.
*
*/
load(buffer: ArrayBuffer, onPiexModuleFailed: VoidCallback):
Promise<PiexLoaderResponse> {
let imageBuffer: ImageBuffer|null;
return piexModuleInitialized()
.then(() => {
if (piexModuleFailed()) {
throw new Error('piex wasm module failed');
}
imageBuffer = new ImageBuffer(buffer);
return imageBuffer.process();
})
.then((result: PiexWasmImageResult) => {
return new PiexLoaderResponse(imageBuffer!.preview(result));
})
.catch((error) => {
if (piexModuleFailed()) {
setTimeout(onPiexModuleFailed, 0);
return Promise.reject('piex wasm module failed');
}
console.warn('[PiexLoader] ' + error);
return Promise.reject(error);
})
.finally(() => {
imageBuffer && imageBuffer.close();
});
},
};
export const PIEX_LOADER_TEST_ONLY = {
getModule: () => PiexModule,
};