chromium/ash/webui/media_app_ui/resources/js/piex_module.ts

// Copyright 2020 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import {PIEX_LOADER_TEST_ONLY, PiexLoader, PiexWasmModule} from './piex_loader.js';

/**
 * Set when PiexLoader has an unrecoverable error to disable future attempts.
 */
let piexEnabled = true;

/** Handles wasm load failures. */
function onPiexModuleFailed() {
  piexEnabled = false;
}

const SIZEOF_APP1_PREFIX = 8;
const SIZEOF_TIFF_HEADER = 8;
const SIZEOF_SINGLE_IFD_FRAME = 18;
const NUM_IFD_FRAMES = 1;

/**
 * For the minimal header, just dump an orientation tag into the "0th" frame and
 * nothing else. There _should_ be other stuff, like a pointer to the "EXIF" IFD
 * frame (containing ExifVersion, etc.), and tags for image resolution. But
 * these are not used by Chrome for rendering.
 */
const SIZEOF_APP1_HEADER = SIZEOF_APP1_PREFIX + SIZEOF_TIFF_HEADER +
    NUM_IFD_FRAMES * SIZEOF_SINGLE_IFD_FRAME;

/**
 * A minimal EXIF header to include an orientation field. Includes the "Start of
 * image" prefix that should already be present, so that this can just form the
 * start of the resulting JPEG; with the quantization table following.
 *
 * The table at https://www.exif.org/Exif2-2.PDF#page=70 was the starting point
 * for this, but details come from all over the document.
 *
 * | Offset | Code |    Meaning      |
 * | ------ | ---- | --------------- |
 * |   -2   | 0xFF | SOI Prefix      |
 * |   -1   | 0xD8 | Start of Image  |
 * |   +0   | 0xFF | Marker Prefix   |
 * |   +1   | 0xE1 | APP1            |
 * |   +2   | 0x.. | Length[1]       |  // Always big-endian.
 * |   +3   | 0x.. | Length[0]       |  // SIZEOF_APP1_HEADER.
 * |   +4   | 0x45 | 'E'             |
 * |   +5   | 0x78 | 'x'             |
 * |   +6   | 0x69 | 'i'             |
 * |   +7   | 0x66 | 'f'             |
 * |   +8   | 0x00 | NULL            |
 * |   +9   | 0x00 | Padding         |
 * |        |      | TIFF header     |  // 8 bytes. Offsets start from here.
 * |   +10  | 0x49 | ByteOrder ('I') |  // Little-endian (seems more common).
 * |   +11  | 0x49 | ByteOrder ('I') |
 * |   +12  | 0x2a | IFD marker[0]   |
 * |   +13  | 0x00 | IFD marker[1]   |
 * |   +14  | 0x08 | IFD offset[0]   |  // 8 (0th IFD immediately follows).
 * |   +15  | 0x00 | IFD offset[1]   |
 * |   +16  | 0x00 | IFD offset[2]   |
 * |   +17  | 0x00 | IFD offset[3]   |
 * |        |      | IFD frame       |  // 18 bytes
 * |   +18  | 0x01 | Field count[0]  |  // 1 Field (just orientation).
 * |   +19  | 0x00 | Field count[1]  |
 * |   +20  | 0x12 | TAG[0]          |  // E.g. TAG_ORIENTATION (0x0112)
 * |   +21  | 0x01 | TAG[1]          |
 * |   +22  | 0x03 | Data type[0]    |  // 3 = "Short"
 * |   +23  | 0x00 | Data type[1]    |
 * |   +24  | 0x01 | Data count[0]   |  // 1
 * |   +25  | 0x00 | Data count[1]   |
 * |   +26  | 0x00 | Data count[2]   |
 * |   +27  | 0x00 | Data count[3]   |
 * | 28-29  | 0x.. | Value           |  // Orientation goes here! <omitted>
 * | 30-31  | 0x00 | Padding         |
 * | 32-35  | 0x00 | Offset to next  |  // 0 to indicate "no more".
 */
const TIFF_HEADER = new Uint8Array([
  0xff,
  0xd8,
  0xff,
  0xe1,
  SIZEOF_APP1_HEADER >> 8,
  SIZEOF_APP1_HEADER & 0xff,
  0x45,
  0x78,
  0x69,
  0x66,
  0x00,
  0x00,
  0x49,
  0x49,
  0x2a,
  0x00,
  0x08,
  0x00,
  0x00,
  0x00,
]);

/**
 * Makes an 18-byte little-endian IFD frame for the Exif orientation value.
 * This is a number [1, 8]. The Exif spec requests a 16-bit unsigned int to be
 * written.
 * Reference: https://www.exif.org/Exif2-2.PDF#page=19.
 */
function makeOrientationIfdFrame(value: number) {
  const LITTLE_ENDIAN = true;
  const TAG_ORIENTATION = 0x0112;
  const TYPE_UINT16 = 3;   // 1=BYTE, 2=ASCII, 3=SHORT, 4=LONG, etc.
  const FIELD_COUNT = 1;   // Just writing orientation.
  const VALUE_LENGTH = 1;  // Writing one "short".
  const NEXT_OFFSET = 0;   // No more frames.
  const buffer = new ArrayBuffer(SIZEOF_SINGLE_IFD_FRAME);
  const view = new DataView(buffer);
  view.setUint16(0, FIELD_COUNT, LITTLE_ENDIAN);
  view.setUint16(2, TAG_ORIENTATION, LITTLE_ENDIAN);
  view.setUint16(4, TYPE_UINT16, LITTLE_ENDIAN);
  view.setUint32(6, VALUE_LENGTH, LITTLE_ENDIAN);

  // The value is <= 4 bytes, so write directly. Otherwise an offset to the
  // value data would be written here. Note the type "hugs" the low-order bits
  // and is followed by padding if the data size is < 4 bytes.
  view.setUint16(10, value, LITTLE_ENDIAN);

  view.setUint32(14, NEXT_OFFSET, LITTLE_ENDIAN);
  return buffer;
}

/**
 * Extracts a JPEG from a RAW Image ArrayBuffer.
 */
async function extractFromRawImageBuffer(buffer: ArrayBuffer) {
  /** Application Segment Marker. */
  const APP1_MARKER = 0xffe1;

  /** SOI. Page 64. */
  const START_OF_IMAGE = 0xffd8;

  /** Field value for no rotation. Don't add an Exif header in this case. */
  const NO_ROTATION = 1;

  if (!piexEnabled) {
    throw new Error('Piex disabled');
  }
  const response = await PiexLoader.load(buffer, onPiexModuleFailed);
  // Note the "thumbnail" is usually the full-sized image "preview", but may
  // fall back to a thumbnail when that is unavailable.
  const jpegData = response.thumbnail;

  function original(warning = '') {
    if (warning) {
      console.warn(`Returning unrotated image: ${warning}.`);
    }
    return new File([jpegData], 'raw-preview', {type: response.mimeType});
  }

  if (response.orientation === NO_ROTATION) {
    return original();
  }

  const view = new DataView(jpegData);
  if (view.getUint16(0) !== START_OF_IMAGE) {
    return original('No SOI');
  }

  // Files returned by Piex should begin immediately with JPEG headers (and the
  // Define Quantization Table marker). If a layer between here and Piex has
  // added its own APP marker segment(s), don't add a duplicate.
  if (view.getUint16(2) === APP1_MARKER) {
    return original('APP1 marker already present');
  }

  // Ignore the Start-Of-Image already in `jpegData` (TIFF_HEADER has one).
  const jpegWithoutSOI = (new Blob([jpegData])).slice(2);

  const orientation = makeOrientationIfdFrame(response.orientation);

  return new File(
      [TIFF_HEADER, orientation, jpegWithoutSOI], 'raw-preview',
      {type: response.mimeType});
}

declare global {
  interface Window {
    getPiexModuleForTesting: () => PiexWasmModule;
    extractFromRawImageBuffer: (buffer: ArrayBuffer) => Promise<File>;
  }
}

// Export to `window` manually until the toolchain has better support for
// dynamic module loading. Dynamic modules require ES2020, but asking chromium's
// closure typechecking toolchain for ES2020 input and output still complains
// that dynamic imports can't be transpiled.
window.extractFromRawImageBuffer = extractFromRawImageBuffer;

// Expose the module on `window` for MediaAppIntegrationTest.HandleRawFiles.
// TODO(b/185957537): Convert the test case to a JS module.
window.getPiexModuleForTesting = PIEX_LOADER_TEST_ONLY.getModule;