From 06e9d10121c460176401ea7c9a700088b04ea4d8 Mon Sep 17 00:00:00 2001 From: Daniele Pelagatti Date: Fri, 31 Oct 2025 13:09:56 +0100 Subject: [PATCH 1/9] feat: removes wasm dependency and implements pure-js JPG encoder --- .github/workflows/ci.yml | 51 ----- .github/workflows/release.yml | 1 - .gitmodules | 3 - .npmignore | 4 - .vscode/launch.json | 21 ++ README.md | 56 +---- eslint.config.mjs | 3 +- examples/encode-and-compress.ts | 2 +- examples/encode-jpeg-metadata.ts | 2 +- examples/worker.ts | 2 +- libultrahdr-wasm | 1 - package.json | 2 +- rollup.config.decodeonly.mjs | 101 --------- rollup.config.mjs | 6 - src/core/types.ts | 6 + src/encode/encode-and-compress.ts | 2 +- src/libultrahdr.ts | 4 +- src/libultrahdr/decode-jpeg-metadata.ts | 79 ------- src/libultrahdr/encode-jpeg-metadata.ts | 58 +++-- src/libultrahdr/jpeg-assembler.ts | 277 ++++++++++++++++++++++++ src/libultrahdr/jpeg-markers.ts | 53 +++++ src/libultrahdr/library.ts | 20 -- src/libultrahdr/mpf-generator.ts | 207 ++++++++++++++++++ src/libultrahdr/xmp-generator.ts | 148 +++++++++++++ tests/encode/encode-and-compress.ts | 2 +- tests/encode/encode.test.ts | 3 + tests/encode/encode.ts | 2 +- tsconfig.json | 2 +- 28 files changed, 766 insertions(+), 352 deletions(-) delete mode 100644 .gitmodules create mode 100644 .vscode/launch.json delete mode 160000 libultrahdr-wasm delete mode 100644 rollup.config.decodeonly.mjs delete mode 100644 src/libultrahdr/decode-jpeg-metadata.ts create mode 100644 src/libultrahdr/jpeg-assembler.ts create mode 100644 src/libultrahdr/jpeg-markers.ts delete mode 100644 src/libultrahdr/library.ts create mode 100644 src/libultrahdr/mpf-generator.ts create mode 100644 src/libultrahdr/xmp-generator.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d1d5175..ab90bed 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,49 +20,6 @@ jobs: steps: - name: 'Checkout' uses: actions/checkout@v4 - with: - submodules: recursive - - - name: 'Setup Emscripten' - uses: mymindstorm/setup-emsdk@v11 - with: - version: 3.1.47 - - - name: 'Setup Python' - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - - name: 'Install Meson & Ninja' - uses: BSFishy/pip-action@v1 - with: - packages: | - meson - ninja - - - name: Write em.txt - uses: "DamianReeves/write-file-action@master" - with: - path: libultrahdr-wasm/em.txt - write-mode: overwrite - contents: | - [binaries] - c = 'emcc' - cpp = 'em++' - ar = 'emar' - nm = 'emnm' - - [host_machine] - system = 'emscripten' - cpu_family = 'wasm32' - cpu = 'wasm32' - endian = 'little' - - - name: 'Build libultrahdr WASM' - run: | - cd libultrahdr-wasm - meson setup build --cross-file=em.txt - meson compile -C build - name: 'Setup Nodejs' uses: actions/setup-node@v3 @@ -82,10 +39,6 @@ jobs: name: build-artifact if-no-files-found: error path: | - libultrahdr-wasm/build/*.ts - libultrahdr-wasm/build/*.js - libultrahdr-wasm/build/*.map - libultrahdr-wasm/build/*.wasm dist/ ################################ @@ -100,8 +53,6 @@ jobs: steps: - name: 'Checkout' uses: actions/checkout@v4 - with: - submodules: recursive - name: 'Download build artifacts' uses: actions/download-artifact@v4 @@ -149,8 +100,6 @@ jobs: - name: 'Checkout' uses: actions/checkout@v4 - with: - submodules: recursive - name: 'Download build artifacts' uses: actions/download-artifact@v4 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 967117c..426b24c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,7 +31,6 @@ jobs: - name: 'Checkout' uses: nschloe/action-cached-lfs-checkout@v1 with: - submodules: recursive token: ${{ secrets.RELEASE_GITHUB_TOKEN }} - name: 'Download build artifacts' diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 04ad2c2..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "libultrahdr-wasm"] - path = libultrahdr-wasm - url = git@github.com:MONOGRID/libultrahdr-wasm.git diff --git a/.npmignore b/.npmignore index 76ff555..18829c0 100644 --- a/.npmignore +++ b/.npmignore @@ -7,7 +7,3 @@ src reports examples wiki -libultrahdr-wasm/**/* -!libultrahdr-wasm/build/*.ts -!libultrahdr-wasm/build/*.js -!libultrahdr-wasm/build/*.wasm diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..617ef68 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,21 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "runtimeExecutable": "/home/daniele/.nvm/versions/node/v22.21.0/bin/node", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}/tests/encode/encode.test.ts", + "outFiles": [ + "${workspaceFolder}/**/*.js" + ] + } + ] +} diff --git a/README.md b/README.md index 9d0861b..27f6f2f 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ const metadata = encodingResult.getMetadata() // embed the compressed images + metadata into a single // JPEG file -const jpeg = await encodeJPEGMetadata({ +const jpeg = encodeJPEGMetadata({ ...encodingResult, ...metadata, sdr, @@ -253,62 +253,14 @@ module.exports = defineConfig({ ``` -## Building with full encoding support (libultrahdr-wasm) +## Building -Clone the repository with git submodules recursively: -```bash -$ git clone --recurse-submodules git@github.com:MONOGRID/gainmap-js.git -``` - -Proceed to build the libultrahdr-wasm module following the [documentation found here](https://github.com/MONOGRID/libultrahdr-wasm#building), here's a quick summary - -```bash -$ cd gainmap-js/libultrahdr-wasm/ -``` -Create a meson "cross compile config" named em.txt and place the following content inside: -```ini -[binaries] -c = 'emcc' -cpp = 'em++' -ar = 'emar' -nm = 'emnm' - -[host_machine] -system = 'emscripten' -cpu_family = 'wasm32' -cpu = 'wasm32' -endian = 'little' -``` -Then execute - -```bash -$ meson setup build --cross-file=em.txt -$ meson compile -C build -``` - -After compiling the WASM, head back to the main repository - -```bash -$ cd .. -$ npm i -$ npm run build -``` - -## Building with no encoding support (requires no wasm) - -> :warning: Building the library with decode only capabilities will not allow to run playwright e2e tests with `npm run test` -> this method should only be used by people who would like to customize the "decoding" part of the library but are unable to build the WASM module for some reason (emscripten can be tricky sometimes, I've been there) - -Clone the repository normally: +Clone the repository: ```bash $ git clone git@github.com:MONOGRID/gainmap-js.git $ cd gainmap-js $ npm i -``` - -build with -```bash -$ npm run build --config rollup.config.decodeonly.mjs +$ npm run build ``` diff --git a/eslint.config.mjs b/eslint.config.mjs index 07a2097..c06bf60 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -29,8 +29,7 @@ const config = defineConfig([ ignores: [ 'node_modules/**/*', 'dist/**/*', - '.vscode/**/*', - 'libultrahdr-wasm/build/**/*' + '.vscode/**/*' ] } ]) diff --git a/examples/encode-and-compress.ts b/examples/encode-and-compress.ts index dcd1024..ff52da9 100644 --- a/examples/encode-and-compress.ts +++ b/examples/encode-and-compress.ts @@ -20,7 +20,7 @@ const encodingResult = await encodeAndCompress({ // embed the compressed images + metadata into a single // JPEG file -const jpeg = await encodeJPEGMetadata({ +const jpeg = encodeJPEGMetadata({ ...encodingResult, sdr: encodingResult.sdr, gainMap: encodingResult.gainMap diff --git a/examples/encode-jpeg-metadata.ts b/examples/encode-jpeg-metadata.ts index 4ec1b61..6ff15c0 100644 --- a/examples/encode-jpeg-metadata.ts +++ b/examples/encode-jpeg-metadata.ts @@ -47,7 +47,7 @@ const metadata = encodingResult.getMetadata() // embed the compressed images + metadata into a single // JPEG file -const jpeg = await encodeJPEGMetadata({ +const jpeg = encodeJPEGMetadata({ ...encodingResult, ...metadata, sdr, diff --git a/examples/worker.ts b/examples/worker.ts index a280c1b..6efcf69 100644 --- a/examples/worker.ts +++ b/examples/worker.ts @@ -30,7 +30,7 @@ const encodingResult = await encodeAndCompress({ // embed the compressed images + metadata into a single // JPEG file -const jpeg = await encodeJPEGMetadata({ +const jpeg = encodeJPEGMetadata({ ...encodingResult, sdr: encodingResult.sdr, gainMap: encodingResult.gainMap diff --git a/libultrahdr-wasm b/libultrahdr-wasm deleted file mode 160000 index b077d0a..0000000 --- a/libultrahdr-wasm +++ /dev/null @@ -1 +0,0 @@ -Subproject commit b077d0a87166f236d13ad505a6a065d646c2b02b diff --git a/package.json b/package.json index ef0934f..81004bf 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@monogrid/gainmap-js", - "version": "3.1.0", + "version": "3.1.0-local", "description": "A Javascript (TypeScript) Port of Adobe Gainmap Technology for storing HDR Images using an SDR Image + a gain map", "keywords": [ "hdr", diff --git a/rollup.config.decodeonly.mjs b/rollup.config.decodeonly.mjs deleted file mode 100644 index 1963a57..0000000 --- a/rollup.config.decodeonly.mjs +++ /dev/null @@ -1,101 +0,0 @@ -import commonjs from '@rollup/plugin-commonjs' -import json from '@rollup/plugin-json' -import resolve from '@rollup/plugin-node-resolve' -import typescript from '@rollup/plugin-typescript' -import { defineConfig } from 'rollup' -import del from 'rollup-plugin-delete' -// @ts-expect-error untyped library -import istanbul from 'rollup-plugin-istanbul' -import license from 'rollup-plugin-license' - -import pkgJSON from './package.json' with { type: 'json' } - -const { author, name, version } = pkgJSON - -/** @type {import('rollup').OutputOptions} */ -const settings = { - globals: { - three: 'three' - }, - sourcemap: !!process.env.PLAYWRIGHT_TESTING -} - -const configBase = defineConfig({ - external: ['three'] -}) - -/** @type {import('rollup').InputPluginOption[]} */ -const plugins = [ - json(), - typescript({ - tsconfig: 'src/tsconfig.json', - declaration: true, - sourceMap: !!process.env.PLAYWRIGHT_TESTING, - declarationDir: 'dist', - include: ['src/**/*.ts'], - exclude: ['src/libultrahdr.ts', 'src/libultrahdr/**/*.ts', 'src/encode.ts', 'src/encode/**/*.ts', 'src/worker*.ts'] - }), - resolve(), - commonjs({ - include: 'node_modules/**', - extensions: ['.js'], - ignoreGlobal: false, - sourceMap: !!process.env.PLAYWRIGHT_TESTING - }), - license({ - banner: ` - ${name} v${version} - With ❤️, by ${author} - ` - }) -] - -if (process.env.PLAYWRIGHT_TESTING) { - plugins.push( - istanbul({ - include: ['src/**/*.ts'] - - }) - ) -} - -/** @type {import('rollup').RollupOptions[]} */ -let configs = [ - defineConfig({ - input: { - decode: './src/decode.ts' - }, - output: { - dir: 'dist', - name, - format: 'es', - ...settings - }, - plugins: [ - del({ targets: 'dist/*' }), - ...plugins - ], - ...configBase - }) -] - -// configs to produce when not testing -// with playwright -if (!process.env.PLAYWRIGHT_TESTING) { - configs = configs.concat([ - // decode UMD - defineConfig({ - input: './src/decode.ts', - output: { - format: 'umd', - name, - file: 'dist/decode.umd.js', - ...settings - }, - plugins, - ...configBase - }) - ]) -} - -export default configs diff --git a/rollup.config.mjs b/rollup.config.mjs index 15d1514..716bef8 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -3,7 +3,6 @@ import json from '@rollup/plugin-json' import resolve from '@rollup/plugin-node-resolve' import typescript from '@rollup/plugin-typescript' import { defineConfig } from 'rollup' -import copy from 'rollup-plugin-copy' import del from 'rollup-plugin-delete' // @ts-expect-error untyped library import istanbul from 'rollup-plugin-istanbul' @@ -77,11 +76,6 @@ let configs = [ }, plugins: [ del({ targets: 'dist/*' }), - copy({ - targets: [ - { src: 'libultrahdr-wasm/build/libultrahdr-esm.wasm', dest: 'dist' } - ] - }), ...plugins ], ...configBase diff --git a/src/core/types.ts b/src/core/types.ts index 5fb3875..da5a999 100644 --- a/src/core/types.ts +++ b/src/core/types.ts @@ -65,6 +65,12 @@ export type GainMapMetadata = { gainMapMax: [number, number, number] } +export type GainMapMetadataExtended = GainMapMetadata & { + version: string + maxContentBoost: number + minContentBoost: number +} + /** * */ diff --git a/src/encode/encode-and-compress.ts b/src/encode/encode-and-compress.ts index c553b17..4232231 100644 --- a/src/encode/encode-and-compress.ts +++ b/src/encode/encode-and-compress.ts @@ -35,7 +35,7 @@ import { CompressedImage, EncodingParametersWithCompression } from './types' * * // embed the compressed images + metadata into a single * // JPEG file - * const jpeg = await encodeJPEGMetadata({ + * const jpeg = encodeJPEGMetadata({ * ...encodingResult, * sdr: encodingResult.sdr, * gainMap: encodingResult.gainMap diff --git a/src/libultrahdr.ts b/src/libultrahdr.ts index fbe9105..b510ae8 100644 --- a/src/libultrahdr.ts +++ b/src/libultrahdr.ts @@ -1,4 +1,2 @@ -export * from '../libultrahdr-wasm/build/libultrahdr' -export * from './libultrahdr/decode-jpeg-metadata' +// Pure JavaScript implementation export * from './libultrahdr/encode-jpeg-metadata' -export * from './libultrahdr/library' diff --git a/src/libultrahdr/decode-jpeg-metadata.ts b/src/libultrahdr/decode-jpeg-metadata.ts deleted file mode 100644 index 2766966..0000000 --- a/src/libultrahdr/decode-jpeg-metadata.ts +++ /dev/null @@ -1,79 +0,0 @@ -import { GainMapMetadata } from '../core/types' -import { getLibrary } from './library' - -/** - * Decodes a JPEG file with an embedded Gainmap and XMP Metadata (aka JPEG-R) - * - * @category Decoding - * @group Decoding - * @deprecated - * @example - * import { decodeJPEGMetadata } from '@monogrid/gainmap-js/libultrahdr' - * - * // fetch a JPEG image containing a gainmap as ArrayBuffer - * const gainmap = new Uint8Array(await (await fetch('gainmap.jpeg')).arrayBuffer()) - * - * // extract data from the JPEG - * const { gainMap, sdr, parsedMetadata } = await decodeJPEGMetadata(gainmap) - * - * @param file A Jpeg file Uint8Array. - * @returns The decoded data - * @throws {Error} if the provided file cannot be parsed or does not contain a valid Gainmap - */ -/* istanbul ignore next */ -export const decodeJPEGMetadata = async (file: Uint8Array) => { - const lib = await getLibrary() - const result = lib.extractJpegR(file, file.length) - if (!result.success) throw new Error(`${result.errorMessage}`) - - const getXMLValue = (xml: string, tag: string, defaultValue?: string): string | [string, string, string] => { - // Check for attribute format first: tag="value" - const attributeMatch = new RegExp(`${tag}="([^"]*)"`, 'i').exec(xml) - if (attributeMatch) return attributeMatch[1] - - // Check for tag format: value or value... - const tagMatch = new RegExp(`<${tag}[^>]*>([\\s\\S]*?)`, 'i').exec(xml) - if (tagMatch) { - // Check if it contains rdf:li elements - const liValues = tagMatch[1].match(/([^<]*)<\/rdf:li>/g) - if (liValues && liValues.length === 3) { - return liValues.map(v => v.replace(/<\/?rdf:li>/g, '')) as [string, string, string] - } - return tagMatch[1].trim() - } - - if (defaultValue !== undefined) return defaultValue - throw new Error(`Can't find ${tag} in gainmap metadata`) - } - - const metadata = result.metadata as string - - const gainMapMin = getXMLValue(metadata, 'hdrgm:GainMapMin', '0') - const gainMapMax = getXMLValue(metadata, 'hdrgm:GainMapMax') - const gamma = getXMLValue(metadata, 'hdrgm:Gamma', '1') - const offsetSDR = getXMLValue(metadata, 'hdrgm:OffsetSDR', '0.015625') - const offsetHDR = getXMLValue(metadata, 'hdrgm:OffsetHDR', '0.015625') - - // These are always attributes, so we can use a simpler regex - const hdrCapacityMinMatch = /hdrgm:HDRCapacityMin="([^"]*)"/.exec(metadata) - const hdrCapacityMin = hdrCapacityMinMatch ? hdrCapacityMinMatch[1] : '0' - - const hdrCapacityMaxMatch = /hdrgm:HDRCapacityMax="([^"]*)"/.exec(metadata) - if (!hdrCapacityMaxMatch) throw new Error('Incomplete gainmap metadata') - const hdrCapacityMax = hdrCapacityMaxMatch[1] - - const parsedMetadata: GainMapMetadata = { - gainMapMin: Array.isArray(gainMapMin) ? gainMapMin.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gainMapMin), parseFloat(gainMapMin), parseFloat(gainMapMin)], - gainMapMax: Array.isArray(gainMapMax) ? gainMapMax.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gainMapMax), parseFloat(gainMapMax), parseFloat(gainMapMax)], - gamma: Array.isArray(gamma) ? gamma.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gamma), parseFloat(gamma), parseFloat(gamma)], - offsetSdr: Array.isArray(offsetSDR) ? offsetSDR.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(offsetSDR), parseFloat(offsetSDR), parseFloat(offsetSDR)], - offsetHdr: Array.isArray(offsetHDR) ? offsetHDR.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(offsetHDR), parseFloat(offsetHDR), parseFloat(offsetHDR)], - hdrCapacityMin: parseFloat(hdrCapacityMin), - hdrCapacityMax: parseFloat(hdrCapacityMax) - } - - return { - ...result, - parsedMetadata - } -} diff --git a/src/libultrahdr/encode-jpeg-metadata.ts b/src/libultrahdr/encode-jpeg-metadata.ts index b0a268d..14c493e 100644 --- a/src/libultrahdr/encode-jpeg-metadata.ts +++ b/src/libultrahdr/encode-jpeg-metadata.ts @@ -1,6 +1,6 @@ -import { type GainMapMetadata } from '../core/types' +import { GainMapMetadata, GainMapMetadataExtended } from '../core/types' import { type CompressedImage } from '../encode/types' -import { getLibrary } from './library' +import { assembleJpegWithGainMap } from './jpeg-assembler' /** * Encapsulates a Gainmap into a single JPEG file (aka: JPEG-R) with the base map @@ -65,7 +65,7 @@ import { getLibrary } from './library' * * // embed the compressed images + metadata into a single * // JPEG file - * const jpeg = await encodeJPEGMetadata({ + * const jpeg = encodeJPEGMetadata({ * ...encodingResult, * ...metadata, * sdr, @@ -75,27 +75,43 @@ import { getLibrary } from './library' * // `jpeg` will be an `Uint8Array` which can be saved somewhere * * - * @param encodingResult - * @returns an Uint8Array representing a JPEG-R file + * @param encodingResult - Encoding result containing SDR image, gain map image, and metadata + * @returns A Uint8Array representing a JPEG-R file * @throws {Error} If `encodingResult.sdr.mimeType !== 'image/jpeg'` * @throws {Error} If `encodingResult.gainMap.mimeType !== 'image/jpeg'` */ -export const encodeJPEGMetadata = async (encodingResult: GainMapMetadata & { sdr: CompressedImage, gainMap: CompressedImage }) => { - const lib = await getLibrary() +export const encodeJPEGMetadata = (encodingResult: GainMapMetadata & { sdr: CompressedImage, gainMap: CompressedImage }): Uint8Array => { + // Validate input + if (encodingResult.sdr.mimeType !== 'image/jpeg') { + throw new Error('This function expects an SDR image compressed in jpeg') + } + if (encodingResult.gainMap.mimeType !== 'image/jpeg') { + throw new Error('This function expects a GainMap image compressed in jpeg') + } - if (encodingResult.sdr.mimeType !== 'image/jpeg') throw new Error('This function expects an SDR image compressed in jpeg') - if (encodingResult.gainMap.mimeType !== 'image/jpeg') throw new Error('This function expects a GainMap image compressed in jpeg') + // Prepare metadata with proper conversions + // The XMP generator handles the log2 conversion internally for gain map min/max values + const metadata: GainMapMetadataExtended = { + version: '1.0', + gainMapMin: encodingResult.gainMapMin, + gainMapMax: encodingResult.gainMapMax, + gamma: encodingResult.gamma, + offsetSdr: encodingResult.offsetSdr, + offsetHdr: encodingResult.offsetHdr, + hdrCapacityMin: encodingResult.hdrCapacityMin, + hdrCapacityMax: encodingResult.hdrCapacityMax, + minContentBoost: Array.isArray(encodingResult.gainMapMin) + ? Math.pow(2, encodingResult.gainMapMin.reduce((a, b) => a + b, 0) / encodingResult.gainMapMin.length) + : Math.pow(2, encodingResult.gainMapMin), + maxContentBoost: Array.isArray(encodingResult.gainMapMax) + ? Math.pow(2, encodingResult.gainMapMax.reduce((a, b) => a + b, 0) / encodingResult.gainMapMax.length) + : Math.pow(2, encodingResult.gainMapMax) + } - return lib.appendGainMap( - encodingResult.sdr.width, encodingResult.sdr.height, - encodingResult.sdr.data, encodingResult.sdr.data.length, - encodingResult.gainMap.data, encodingResult.gainMap.data.length, - encodingResult.gainMapMax.reduce((p, n) => p + n, 0) / encodingResult.gainMapMax.length, - encodingResult.gainMapMin.reduce((p, n) => p + n, 0) / encodingResult.gainMapMin.length, - encodingResult.gamma.reduce((p, n) => p + n, 0) / encodingResult.gamma.length, - encodingResult.offsetSdr.reduce((p, n) => p + n, 0) / encodingResult.offsetSdr.length, - encodingResult.offsetHdr.reduce((p, n) => p + n, 0) / encodingResult.offsetHdr.length, - encodingResult.hdrCapacityMin, - encodingResult.hdrCapacityMax - ) as Uint8Array + // Assemble the JPEG with gain map using pure JavaScript + return assembleJpegWithGainMap({ + sdr: encodingResult.sdr, + gainMap: encodingResult.gainMap, + metadata + }) } diff --git a/src/libultrahdr/jpeg-assembler.ts b/src/libultrahdr/jpeg-assembler.ts new file mode 100644 index 0000000..84b255b --- /dev/null +++ b/src/libultrahdr/jpeg-assembler.ts @@ -0,0 +1,277 @@ +/** + * JPEG assembler for creating JPEG-R (JPEG with gain map) files + * Based on libultrahdr jpegr.cpp implementation + */ + +import { GainMapMetadataExtended } from '../core/types' +import { type CompressedImage } from '../encode/types' +import { MARKER_PREFIX, MARKERS, XMP_NAMESPACE } from './jpeg-markers' +import { calculateMpfSize, generateMpf } from './mpf-generator' +import { generateXmpForPrimaryImage, generateXmpForSecondaryImage } from './xmp-generator' + +/** + * Options for assembling a JPEG with gain map + */ +export interface AssembleJpegOptions { + /** Primary (SDR) JPEG image */ + sdr: CompressedImage + /** Gain map JPEG image */ + gainMap: CompressedImage + /** Gain map metadata */ + metadata: GainMapMetadataExtended + /** Optional EXIF data to embed */ + exif?: Uint8Array + /** Optional ICC color profile */ + icc?: Uint8Array +} + +/** + * Extract EXIF data from a JPEG if present + * + * @param jpegData - JPEG file data + * @returns Object containing EXIF data and position, or null if not found + */ +function extractExif (jpegData: Uint8Array): { data: Uint8Array, pos: number, size: number } | null { + const view = new DataView(jpegData.buffer, jpegData.byteOffset, jpegData.byteLength) + + // Check for JPEG SOI marker + if (view.getUint8(0) !== MARKER_PREFIX || view.getUint8(1) !== MARKERS.SOI) { + return null + } + + let offset = 2 + const EXIF_SIGNATURE = 'Exif\0\0' + + while (offset < jpegData.length - 1) { + // Check for marker prefix + if (view.getUint8(offset) !== MARKER_PREFIX) { + break + } + + const marker = view.getUint8(offset + 1) + + // Check for SOS (Start of Scan) - end of metadata + if (marker === MARKERS.SOS) { + break + } + + // Check for APP1 marker (EXIF/XMP) + if (marker === MARKERS.APP1) { + const length = view.getUint16(offset + 2, false) // Big endian + const dataStart = offset + 4 + + // Check if this APP1 contains EXIF + let isExif = true + for (let i = 0; i < EXIF_SIGNATURE.length; i++) { + if (dataStart + i >= jpegData.length || jpegData[dataStart + i] !== EXIF_SIGNATURE.charCodeAt(i)) { + isExif = false + break + } + } + + if (isExif) { + // Found EXIF data + const exifSize = length - 2 // Length includes the 2-byte length field itself + const exifData = jpegData.slice(dataStart, dataStart + exifSize) + return { + data: exifData, + pos: offset, + size: length + 2 // Include marker (2 bytes) + length (2 bytes) + data + } + } + } + + // Move to next marker + const length = view.getUint16(offset + 2, false) + offset += 2 + length + } + + return null +} + +/** + * Copy JPEG data without EXIF segment + * + * @param jpegData - Original JPEG data + * @param exifPos - Position of EXIF segment + * @param exifSize - Size of EXIF segment (including marker and length) + * @returns JPEG data without EXIF + */ +function copyJpegWithoutExif (jpegData: Uint8Array, exifPos: number, exifSize: number): Uint8Array { + const newSize = jpegData.length - exifSize + const result = new Uint8Array(newSize) + + // Copy data before EXIF + result.set(jpegData.subarray(0, exifPos), 0) + + // Copy data after EXIF + result.set(jpegData.subarray(exifPos + exifSize), exifPos) + + return result +} + +/** + * Write a JPEG marker and its data + * + * @param buffer - Target buffer + * @param pos - Current position in buffer + * @param marker - Marker type (without 0xFF prefix) + * @param data - Data to write after marker + * @returns New position after writing + */ +function writeMarker (buffer: Uint8Array, pos: number, marker: number, data?: Uint8Array): number { + const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) + + // Write marker + view.setUint8(pos++, MARKER_PREFIX) + view.setUint8(pos++, marker) + + // Write data if present + if (data && data.length > 0) { + // Write length (big endian, includes the 2-byte length field itself) + const length = data.length + 2 + view.setUint16(pos, length, false) + pos += 2 + + // Write data + buffer.set(data, pos) + pos += data.length + } + + return pos +} + +/** + * Assemble a JPEG-R file (JPEG with embedded gain map) + * + * The structure is: + * 1. Primary image: + * - SOI + * - APP1 (EXIF if present) + * - APP1 (XMP with gain map metadata) + * - APP2 (ICC profile if present) + * - APP2 (MPF data) + * - Rest of primary JPEG data + * 2. Secondary image (gain map): + * - SOI + * - APP1 (XMP with gain map parameters) + * - Rest of gain map JPEG data + * + * @param options - Assembly options + * @returns Complete JPEG-R file as Uint8Array + */ +export function assembleJpegWithGainMap (options: AssembleJpegOptions): Uint8Array { + const { sdr, gainMap, metadata, exif: externalExif, icc } = options + + // Validate input + if (sdr.mimeType !== 'image/jpeg') { + throw new Error('SDR image must be JPEG format') + } + if (gainMap.mimeType !== 'image/jpeg') { + throw new Error('Gain map image must be JPEG format') + } + + // Check for EXIF in primary image + const exifFromJpeg = extractExif(sdr.data) + + if (exifFromJpeg && externalExif) { + throw new Error('Primary image already contains EXIF data, cannot add external EXIF') + } + + // Prepare primary JPEG (remove embedded EXIF if present) + let primaryJpegData = sdr.data + let exifData = externalExif + + if (exifFromJpeg) { + primaryJpegData = copyJpegWithoutExif(sdr.data, exifFromJpeg.pos, exifFromJpeg.size) + exifData = exifFromJpeg.data + } + + // Generate XMP for secondary image + const xmpSecondary = generateXmpForSecondaryImage(metadata) + const xmpSecondaryBytes = new TextEncoder().encode(xmpSecondary) + + // Calculate secondary image size + // 2 bytes SOI + 2 bytes marker + 2 bytes length field + namespace + XMP data + gain map data (without SOI) + const namespaceBytes = new TextEncoder().encode(XMP_NAMESPACE) + const secondaryImageSize = 2 + 2 + 2 + namespaceBytes.length + xmpSecondaryBytes.length + (gainMap.data.length - 2) + + // Generate XMP for primary image + const xmpPrimary = generateXmpForPrimaryImage(secondaryImageSize, metadata) + const xmpPrimaryBytes = new TextEncoder().encode(xmpPrimary) + const xmpPrimaryData = new Uint8Array(namespaceBytes.length + xmpPrimaryBytes.length) + xmpPrimaryData.set(namespaceBytes, 0) + xmpPrimaryData.set(xmpPrimaryBytes, namespaceBytes.length) + + // Calculate MPF size and offset + const mpfLength = calculateMpfSize() + + // Calculate total size + let totalSize = 2 // SOI + if (exifData) totalSize += 2 + 2 + exifData.length // APP1 + length + EXIF + totalSize += 2 + 2 + xmpPrimaryData.length // APP1 + length + XMP primary + if (icc) totalSize += 2 + 2 + icc.length // APP2 + length + ICC + totalSize += 2 + 2 + mpfLength // APP2 + length + MPF + totalSize += primaryJpegData.length - 2 // Primary JPEG without SOI + totalSize += secondaryImageSize // Secondary image + + // Calculate offsets for MPF + const primaryImageSize = totalSize - secondaryImageSize + // Offset is from MP Endian field (after APP2 marker + length + MPF signature) + const secondaryImageOffset = primaryImageSize - ( + 2 + // SOI + (exifData ? 2 + 2 + exifData.length : 0) + + 2 + 2 + xmpPrimaryData.length + + (icc ? 2 + 2 + icc.length : 0) + + 2 + 2 + 4 // APP2 marker + length + MPF signature + ) + + // Generate MPF data + const mpfDataActual = generateMpf(primaryImageSize, 0, secondaryImageSize, secondaryImageOffset) + + // Allocate output buffer + const output = new Uint8Array(totalSize) + let pos = 0 + + // === PRIMARY IMAGE === + + // Write SOI + pos = writeMarker(output, pos, MARKERS.SOI) + + // Write EXIF if present + if (exifData) { + pos = writeMarker(output, pos, MARKERS.APP1, exifData) + } + + // Write XMP for primary image (already created above) + pos = writeMarker(output, pos, MARKERS.APP1, xmpPrimaryData) + + // Write ICC profile if present + if (icc) { + pos = writeMarker(output, pos, MARKERS.APP2, icc) + } + + // Write MPF + pos = writeMarker(output, pos, MARKERS.APP2, mpfDataActual) + + // Write rest of primary JPEG (skip SOI) + output.set(primaryJpegData.subarray(2), pos) + pos += primaryJpegData.length - 2 + + // === SECONDARY IMAGE (GAIN MAP) === + + // Write SOI + pos = writeMarker(output, pos, MARKERS.SOI) + + // Write XMP for secondary image + const xmpSecondaryData = new Uint8Array(namespaceBytes.length + xmpSecondaryBytes.length) + xmpSecondaryData.set(namespaceBytes, 0) + xmpSecondaryData.set(xmpSecondaryBytes, namespaceBytes.length) + pos = writeMarker(output, pos, MARKERS.APP1, xmpSecondaryData) + + // Write rest of gain map JPEG (skip SOI) + output.set(gainMap.data.subarray(2), pos) + pos += gainMap.data.length - 2 + + return output +} diff --git a/src/libultrahdr/jpeg-markers.ts b/src/libultrahdr/jpeg-markers.ts new file mode 100644 index 0000000..d1da877 --- /dev/null +++ b/src/libultrahdr/jpeg-markers.ts @@ -0,0 +1,53 @@ +/** + * JPEG marker constants + * Based on JPEG specification and libultrahdr implementation + */ + +/** + * JPEG marker prefix - all markers start with this byte + */ +export const MARKER_PREFIX = 0xff + +/** + * JPEG markers + */ +export const MARKERS = { + /** Start of Image */ + SOI: 0xd8, + /** End of Image */ + EOI: 0xd9, + /** Application segment 0 */ + APP0: 0xe0, + /** Application segment 1 (EXIF/XMP) */ + APP1: 0xe1, + /** Application segment 2 (ICC/MPF) */ + APP2: 0xe2, + /** Start of Scan */ + SOS: 0xda, + /** Define Quantization Table */ + DQT: 0xdb, + /** Define Huffman Table */ + DHT: 0xc4, + /** Start of Frame (baseline DCT) */ + SOF0: 0xc0 +} as const + +/** + * XMP namespace identifier for APP1 marker + */ +export const XMP_NAMESPACE = 'http://ns.adobe.com/xap/1.0/\0' + +/** + * EXIF identifier for APP1 marker + */ +export const EXIF_IDENTIFIER = 'Exif\0\0' + +/** + * MPF signature for APP2 marker + */ +export const MPF_SIGNATURE = 'MPF\0' + +/** + * ICC profile identifier for APP2 marker + */ +export const ICC_IDENTIFIER = 'ICC_PROFILE\0' diff --git a/src/libultrahdr/library.ts b/src/libultrahdr/library.ts deleted file mode 100644 index 9e35ab4..0000000 --- a/src/libultrahdr/library.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { MainModule } from '../../libultrahdr-wasm/build/libultrahdr' -// @ts-expect-error untyped -import libultrahdr from '../../libultrahdr-wasm/build/libultrahdr-esm' - -let library: MainModule | undefined - -/** - * Instances the WASM module and returns it, only one module will be created upon multiple calls. - * @category WASM - * @group WASM - * - * @returns - */ -export const getLibrary = async () => { - if (!library) { - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - library = await libultrahdr() as MainModule - } - return library -} diff --git a/src/libultrahdr/mpf-generator.ts b/src/libultrahdr/mpf-generator.ts new file mode 100644 index 0000000..3c820ce --- /dev/null +++ b/src/libultrahdr/mpf-generator.ts @@ -0,0 +1,207 @@ +/** + * Multi-Picture Format (MPF) generator + * Based on CIPA DC-007 specification and libultrahdr multipictureformat.cpp + * + * MPF is used to embed multiple images in a single JPEG file + */ + +/** + * MPF constants from the specification + */ +const MPF_CONSTANTS = { + /** MPF signature "MPF\0" */ + SIGNATURE: new Uint8Array([0x4d, 0x50, 0x46, 0x00]), + + /** Big endian marker "MM" */ + BIG_ENDIAN: new Uint8Array([0x4d, 0x4d]), + + /** Little endian marker "II" */ + LITTLE_ENDIAN: new Uint8Array([0x49, 0x49]), + + /** TIFF magic number */ + TIFF_MAGIC: 0x002a, + + /** Number of pictures in MPF */ + NUM_PICTURES: 2, + + /** Number of tags to serialize */ + TAG_COUNT: 3, + + /** Size of each tag in bytes */ + TAG_SIZE: 12, + + /** Size of each MP entry in bytes */ + MP_ENTRY_SIZE: 16 +} as const + +/** + * MPF tag identifiers + */ +const MPF_TAGS = { + /** MPF version tag */ + VERSION: 0xb000, + + /** Number of images tag */ + NUMBER_OF_IMAGES: 0xb001, + + /** MP entry tag */ + MP_ENTRY: 0xb002 +} as const + +/** + * MPF tag types + */ +const MPF_TAG_TYPES = { + /** Undefined type */ + UNDEFINED: 7, + + /** Unsigned long type */ + ULONG: 4 +} as const + +/** + * MP entry attributes + */ +const MP_ENTRY_ATTRIBUTES = { + /** JPEG format */ + FORMAT_JPEG: 0x00000000, + + /** Primary image type */ + TYPE_PRIMARY: 0x20000000 +} as const + +/** + * MPF version string + */ +const MPF_VERSION = new Uint8Array([0x30, 0x31, 0x30, 0x30]) // "0100" + +/** + * Calculate the total size of the MPF structure + */ +export function calculateMpfSize (): number { + return ( + MPF_CONSTANTS.SIGNATURE.length + // Signature "MPF\0" + 2 + // Endianness marker + 2 + // TIFF magic number + 4 + // Index IFD Offset + 2 + // Tag count + MPF_CONSTANTS.TAG_COUNT * MPF_CONSTANTS.TAG_SIZE + // Tags + 4 + // Attribute IFD offset + MPF_CONSTANTS.NUM_PICTURES * MPF_CONSTANTS.MP_ENTRY_SIZE // MP Entries + ) +} + +/** + * Generate MPF (Multi-Picture Format) data structure + * + * @param primaryImageSize - Size of the primary image in bytes + * @param primaryImageOffset - Offset of the primary image (typically 0 for FII - First Individual Image) + * @param secondaryImageSize - Size of the secondary (gain map) image in bytes + * @param secondaryImageOffset - Offset of the secondary image from the MP Endian field + * @returns Uint8Array containing the MPF data + */ +export function generateMpf ( + primaryImageSize: number, + primaryImageOffset: number, + secondaryImageSize: number, + secondaryImageOffset: number +): Uint8Array { + const mpfSize = calculateMpfSize() + const buffer = new ArrayBuffer(mpfSize) + const view = new DataView(buffer) + const uint8View = new Uint8Array(buffer) + + let pos = 0 + + // Write MPF signature "MPF\0" + uint8View.set(MPF_CONSTANTS.SIGNATURE, pos) + pos += MPF_CONSTANTS.SIGNATURE.length + + // Write endianness marker (big endian "MM") + // Using big endian to match the C++ implementation's USE_BIG_ENDIAN + uint8View.set(MPF_CONSTANTS.BIG_ENDIAN, pos) + const bigEndian = false // DataView uses little endian by default, so we need to flip this + pos += 2 + + // Write TIFF magic number (0x002A) + view.setUint16(pos, MPF_CONSTANTS.TIFF_MAGIC, bigEndian) + pos += 2 + + // Set the Index IFD offset + // This offset is from the start of the TIFF header (the endianness marker) + // After: endianness (2) + magic (2) + this offset field (4) = 8 bytes + const indexIfdOffset = 8 + view.setUint32(pos, indexIfdOffset, bigEndian) + pos += 4 + + // Write tag count (3 tags: version, number of images, MP entries) + view.setUint16(pos, MPF_CONSTANTS.TAG_COUNT, bigEndian) + pos += 2 + + // Write version tag + view.setUint16(pos, MPF_TAGS.VERSION, bigEndian) + pos += 2 + view.setUint16(pos, MPF_TAG_TYPES.UNDEFINED, bigEndian) + pos += 2 + view.setUint32(pos, MPF_VERSION.length, bigEndian) + pos += 4 + uint8View.set(MPF_VERSION, pos) + pos += 4 // Version is 4 bytes, embedded in the tag + + // Write number of images tag + view.setUint16(pos, MPF_TAGS.NUMBER_OF_IMAGES, bigEndian) + pos += 2 + view.setUint16(pos, MPF_TAG_TYPES.ULONG, bigEndian) + pos += 2 + view.setUint32(pos, 1, bigEndian) // Count = 1 + pos += 4 + view.setUint32(pos, MPF_CONSTANTS.NUM_PICTURES, bigEndian) + pos += 4 + + // Write MP entry tag + view.setUint16(pos, MPF_TAGS.MP_ENTRY, bigEndian) + pos += 2 + view.setUint16(pos, MPF_TAG_TYPES.UNDEFINED, bigEndian) + pos += 2 + view.setUint32(pos, MPF_CONSTANTS.MP_ENTRY_SIZE * MPF_CONSTANTS.NUM_PICTURES, bigEndian) + pos += 4 + + // Calculate MP entry offset + // The offset is from the start of the MP Endian field (after signature) + // Current position is at the value field of MP Entry tag + const mpEntryOffset = pos - MPF_CONSTANTS.SIGNATURE.length + 4 + 4 + view.setUint32(pos, mpEntryOffset, bigEndian) + pos += 4 + + // Write attribute IFD offset (0 = none) + view.setUint32(pos, 0, bigEndian) + pos += 4 + + // Write MP entries for primary image + // Attribute format: JPEG (0x00000000) | Type: Primary (0x20000000) + view.setUint32(pos, MP_ENTRY_ATTRIBUTES.FORMAT_JPEG | MP_ENTRY_ATTRIBUTES.TYPE_PRIMARY, bigEndian) + pos += 4 + view.setUint32(pos, primaryImageSize, bigEndian) + pos += 4 + view.setUint32(pos, primaryImageOffset, bigEndian) + pos += 4 + view.setUint16(pos, 0, bigEndian) // Dependent image 1 + pos += 2 + view.setUint16(pos, 0, bigEndian) // Dependent image 2 + pos += 2 + + // Write MP entries for secondary image (gain map) + // Attribute format: JPEG only (no type flag) + view.setUint32(pos, MP_ENTRY_ATTRIBUTES.FORMAT_JPEG, bigEndian) + pos += 4 + view.setUint32(pos, secondaryImageSize, bigEndian) + pos += 4 + view.setUint32(pos, secondaryImageOffset, bigEndian) + pos += 4 + view.setUint16(pos, 0, bigEndian) // Dependent image 1 + pos += 2 + view.setUint16(pos, 0, bigEndian) // Dependent image 2 + pos += 2 + + return uint8View +} diff --git a/src/libultrahdr/xmp-generator.ts b/src/libultrahdr/xmp-generator.ts new file mode 100644 index 0000000..a3026fe --- /dev/null +++ b/src/libultrahdr/xmp-generator.ts @@ -0,0 +1,148 @@ +/** + * XMP metadata generator for gain map images + * Based on libultrahdr jpegrutils.cpp implementation + */ + +import { type GainMapMetadataExtended } from '../core/types' + +/** + * Item semantic types + */ +const ITEM_SEMANTIC = { + PRIMARY: 'Primary', + GAIN_MAP: 'GainMap' +} as const + +/** + * MIME type for JPEG images + */ +const MIME_IMAGE_JPEG = 'image/jpeg' + +/** + * Escape XML special characters + */ +function escapeXml (str: string | number): string { + return String(str) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +/** + * Generate XMP metadata for the primary image + * + * This XMP contains: + * - Container directory with references to primary and gain map images + * - Gain map version + * - Item metadata for both images + * + * @param secondaryImageLength - Length of the secondary (gain map) JPEG in bytes + * @param metadata - Gain map metadata + * @returns XMP packet as string + */ +export function generateXmpForPrimaryImage ( + secondaryImageLength: number, + metadata: GainMapMetadataExtended +): string { + const lines: string[] = [] + + // XMP packet header + lines.push('') + lines.push('') + lines.push(' ') + lines.push(' ') + + // Container directory + lines.push(' ') + lines.push(' ') + + // Primary image item + lines.push(' ') + lines.push(' `) + lines.push(' ') + + // Gain map image item + lines.push(' ') + lines.push(' `) + lines.push(' ') + + lines.push(' ') + lines.push(' ') + lines.push(' ') + lines.push(' ') + lines.push('') + lines.push('') + + return lines.join('\n') +} + +/** + * Generate XMP metadata for the secondary (gain map) image + * + * This XMP contains all the gain map parameters: + * - Version + * - Gain map min/max + * - Gamma + * - Offset SDR/HDR + * - HDR capacity min/max + * - Base rendition flag + * + * @param metadata - Gain map metadata + * @returns XMP packet as string + */ +export function generateXmpForSecondaryImage (metadata: GainMapMetadataExtended): string { + const lines: string[] = [] + + // hdrCapacityMin/Max are already in log2 space (from GainMapEncoderMaterial) + // No conversion needed + const hdrCapacityMin = metadata.hdrCapacityMin + const hdrCapacityMax = metadata.hdrCapacityMax + + // Handle array values - take average if array, or use single value + const getAverage = (val: number | [number, number, number]): number => { + if (Array.isArray(val)) { + return val.reduce((sum, v) => sum + v, 0) / val.length + } + return val + } + + const gainMapMinAvg = getAverage(metadata.gainMapMin) + const gainMapMaxAvg = getAverage(metadata.gainMapMax) + const gammaAvg = getAverage(metadata.gamma) + const offsetSdrAvg = getAverage(metadata.offsetSdr) + const offsetHdrAvg = getAverage(metadata.offsetHdr) + + // XMP packet header + lines.push('') + lines.push('') + lines.push(' ') + lines.push(' ') + lines.push(' ') + lines.push('') + lines.push('') + + return lines.join('\n') +} diff --git a/tests/encode/encode-and-compress.ts b/tests/encode/encode-and-compress.ts index d622aaa..1cf705a 100644 --- a/tests/encode/encode-and-compress.ts +++ b/tests/encode/encode-and-compress.ts @@ -40,7 +40,7 @@ export const encodeAndCompressInBrowser = async (args: Omit { expect(meta!.gainMapMin, 'gainMapMin is not default value').toEqual([0, 0, 0]) // default value const extracted = await page.evaluate(testMPFExtractorInBrowser, result.jpeg) + // await fs.writeFile('result.jpg', Buffer.from(result.jpeg)) + // await fs.writeFile('extracted-0.jpg', Buffer.from(extracted[0])) + // await fs.writeFile('extracted-1.jpg', Buffer.from(extracted[1])) const resized = await sharp(Buffer.from(extracted[0])) .resize({ width: 500, height: 500, fit: 'inside' }) diff --git a/tests/encode/encode.ts b/tests/encode/encode.ts index 77a67cd..08cb568 100644 --- a/tests/encode/encode.ts +++ b/tests/encode/encode.ts @@ -77,7 +77,7 @@ export const encodeInBrowser = async (args: Omit Date: Fri, 31 Oct 2025 14:58:43 +0100 Subject: [PATCH 2/9] chore: fix package-lock --- package-lock.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index eb17e8a..24c9489 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@monogrid/gainmap-js", - "version": "3.1.0", + "version": "3.1.0-local", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@monogrid/gainmap-js", - "version": "3.1.0", + "version": "3.1.0-local", "license": "MIT", "dependencies": { "promise-worker-transferable": "^1.0.4" From 476bb99cda673368b8f2f3011e6a9c1bbec1044d Mon Sep 17 00:00:00 2001 From: Daniele Pelagatti Date: Fri, 31 Oct 2025 15:19:13 +0100 Subject: [PATCH 3/9] fix: some code quality stuff --- src/libultrahdr/jpeg-assembler.ts | 2 +- src/libultrahdr/mpf-generator.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libultrahdr/jpeg-assembler.ts b/src/libultrahdr/jpeg-assembler.ts index 84b255b..c77284d 100644 --- a/src/libultrahdr/jpeg-assembler.ts +++ b/src/libultrahdr/jpeg-assembler.ts @@ -271,7 +271,7 @@ export function assembleJpegWithGainMap (options: AssembleJpegOptions): Uint8Arr // Write rest of gain map JPEG (skip SOI) output.set(gainMap.data.subarray(2), pos) - pos += gainMap.data.length - 2 + // pos += gainMap.data.length - 2 return output } diff --git a/src/libultrahdr/mpf-generator.ts b/src/libultrahdr/mpf-generator.ts index 3c820ce..29047eb 100644 --- a/src/libultrahdr/mpf-generator.ts +++ b/src/libultrahdr/mpf-generator.ts @@ -201,7 +201,7 @@ export function generateMpf ( view.setUint16(pos, 0, bigEndian) // Dependent image 1 pos += 2 view.setUint16(pos, 0, bigEndian) // Dependent image 2 - pos += 2 + // pos += 2 return uint8View } From e18393b0ea44e09a99a46ac07459e7eb700d493d Mon Sep 17 00:00:00 2001 From: Daniele Pelagatti Date: Fri, 31 Oct 2025 15:20:28 +0100 Subject: [PATCH 4/9] ci: restore some CI parameters --- .github/workflows/ci.yml | 9 ++++++++- .github/workflows/release.yml | 1 + 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ab90bed..1c5ddaa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: steps: - name: 'Checkout' uses: actions/checkout@v4 + with: + submodules: recursive - name: 'Setup Nodejs' uses: actions/setup-node@v3 @@ -53,11 +55,14 @@ jobs: steps: - name: 'Checkout' uses: actions/checkout@v4 + with: + submodules: recursive - name: 'Download build artifacts' uses: actions/download-artifact@v4 with: name: build-artifact + path: . - name: 'Setup Nodejs' uses: actions/setup-node@v3 @@ -100,11 +105,13 @@ jobs: - name: 'Checkout' uses: actions/checkout@v4 - + with: + submodules: recursive - name: 'Download build artifacts' uses: actions/download-artifact@v4 with: name: build-artifact + path: . - name: 'Setup Nodejs' uses: actions/setup-node@v3 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 426b24c..967117c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -31,6 +31,7 @@ jobs: - name: 'Checkout' uses: nschloe/action-cached-lfs-checkout@v1 with: + submodules: recursive token: ${{ secrets.RELEASE_GITHUB_TOKEN }} - name: 'Download build artifacts' From 4bebb90be4e26539b40234852e6891828000aa22 Mon Sep 17 00:00:00 2001 From: Daniele Pelagatti Date: Fri, 31 Oct 2025 15:50:40 +0100 Subject: [PATCH 5/9] ci: maybe now fixed problem --- .github/workflows/ci.yml | 4 ++-- .github/workflows/publish-wiki.yml | 1 + .github/workflows/release.yml | 1 + .github/workflows/report.yml | 1 + 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c5ddaa..2f3e238 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -62,7 +62,7 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifact - path: . + path: dist - name: 'Setup Nodejs' uses: actions/setup-node@v3 @@ -111,7 +111,7 @@ jobs: uses: actions/download-artifact@v4 with: name: build-artifact - path: . + path: dist - name: 'Setup Nodejs' uses: actions/setup-node@v3 diff --git a/.github/workflows/publish-wiki.yml b/.github/workflows/publish-wiki.yml index dc3fb4d..fc74c95 100644 --- a/.github/workflows/publish-wiki.yml +++ b/.github/workflows/publish-wiki.yml @@ -30,6 +30,7 @@ jobs: uses: dawidd6/action-download-artifact@v6 with: name: build-artifact + path: dist run_id: ${{ github.event.workflow_run.id }} workflow_conclusion: success diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 967117c..849af03 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -38,6 +38,7 @@ jobs: uses: dawidd6/action-download-artifact@v6 # download artifacts with: name: build-artifact + path: dist run_id: ${{ github.event.workflow_run.id }} workflow_conclusion: success diff --git a/.github/workflows/report.yml b/.github/workflows/report.yml index 1f6c37a..14273de 100644 --- a/.github/workflows/report.yml +++ b/.github/workflows/report.yml @@ -25,6 +25,7 @@ jobs: uses: dawidd6/action-download-artifact@v6 # download artifacts with: name: build-artifact + path: dist run_id: ${{ github.event.workflow_run.id }} - name: 'Download check artifacts' From 55b40d89a6c5c0913e9277470aeb25d092741d96 Mon Sep 17 00:00:00 2001 From: Daniele Pelagatti Date: Mon, 17 Nov 2025 17:47:57 +0100 Subject: [PATCH 6/9] chore: a test for typescript reports and eslint --- examples/compress.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/examples/compress.ts b/examples/compress.ts index 6a5e80d..720a76b 100644 --- a/examples/compress.ts +++ b/examples/compress.ts @@ -1,6 +1,9 @@ /* eslint-disable unused-imports/no-unused-vars */ import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js/encode' -import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' +import { EXRLoader, IntentionalTypescriptError } from 'three/examples/jsm/loaders/EXRLoader.js' + + + // load an HDR file const loader = new EXRLoader() From 544444d3476aa2bf9923a1b6f2f0eef344cede1b Mon Sep 17 00:00:00 2001 From: Daniele Pelagatti Date: Mon, 17 Nov 2025 17:48:11 +0100 Subject: [PATCH 7/9] Revert "chore: a test for typescript reports and eslint" This reverts commit 55b40d89a6c5c0913e9277470aeb25d092741d96. --- examples/compress.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/compress.ts b/examples/compress.ts index 720a76b..6a5e80d 100644 --- a/examples/compress.ts +++ b/examples/compress.ts @@ -1,9 +1,6 @@ /* eslint-disable unused-imports/no-unused-vars */ import { compress, encode, findTextureMinMax } from '@monogrid/gainmap-js/encode' -import { EXRLoader, IntentionalTypescriptError } from 'three/examples/jsm/loaders/EXRLoader.js' - - - +import { EXRLoader } from 'three/examples/jsm/loaders/EXRLoader.js' // load an HDR file const loader = new EXRLoader() From 3d000024f16fcce88f200d3db1e782e72bb803c2 Mon Sep 17 00:00:00 2001 From: Daniele Pelagatti Date: Wed, 19 Nov 2025 11:18:15 +0100 Subject: [PATCH 8/9] fix: fixes some types for frontends --- src/encode/compress.ts | 6 ++++-- src/encode/types.ts | 4 ++-- src/libultrahdr/encode-jpeg-metadata.ts | 2 +- src/libultrahdr/jpeg-assembler.ts | 12 ++++++------ src/libultrahdr/mpf-generator.ts | 7 +------ 5 files changed, 14 insertions(+), 17 deletions(-) diff --git a/src/encode/compress.ts b/src/encode/compress.ts index fc05922..e94bbd5 100644 --- a/src/encode/compress.ts +++ b/src/encode/compress.ts @@ -43,7 +43,7 @@ export const compress = async (params: CompressParameters): Promise | Uint8ClampedArray)], { type: params.sourceMimeType }) + imageBitmapSource = new Blob([source], { type: params.sourceMimeType }) } else if (source instanceof ImageData) { imageBitmapSource = source } else { @@ -61,7 +61,9 @@ export const compress = async (params: CompressParameters): Promise mimeType: CompressionMimeType width: number height: number @@ -167,7 +167,7 @@ export type CompressParameters = CompressOptions & ({ /** * Encoded Image Data with a mimeType */ - source: Uint8Array | Uint8ClampedArray + source: Uint8Array | Uint8ClampedArray /** * mimeType of the encoded input */ diff --git a/src/libultrahdr/encode-jpeg-metadata.ts b/src/libultrahdr/encode-jpeg-metadata.ts index aa4400c..0499aec 100644 --- a/src/libultrahdr/encode-jpeg-metadata.ts +++ b/src/libultrahdr/encode-jpeg-metadata.ts @@ -80,7 +80,7 @@ import { assembleJpegWithGainMap } from './jpeg-assembler' * @throws {Error} If `encodingResult.sdr.mimeType !== 'image/jpeg'` * @throws {Error} If `encodingResult.gainMap.mimeType !== 'image/jpeg'` */ -export const encodeJPEGMetadata = (encodingResult: GainMapMetadata & { sdr: CompressedImage, gainMap: CompressedImage }): Uint8Array => { +export const encodeJPEGMetadata = (encodingResult: GainMapMetadata & { sdr: CompressedImage, gainMap: CompressedImage }) => { // Validate input if (encodingResult.sdr.mimeType !== 'image/jpeg') { throw new Error('This function expects an SDR image compressed in jpeg') diff --git a/src/libultrahdr/jpeg-assembler.ts b/src/libultrahdr/jpeg-assembler.ts index c77284d..bc29272 100644 --- a/src/libultrahdr/jpeg-assembler.ts +++ b/src/libultrahdr/jpeg-assembler.ts @@ -20,9 +20,9 @@ export interface AssembleJpegOptions { /** Gain map metadata */ metadata: GainMapMetadataExtended /** Optional EXIF data to embed */ - exif?: Uint8Array + exif?: Uint8Array /** Optional ICC color profile */ - icc?: Uint8Array + icc?: Uint8Array } /** @@ -31,7 +31,7 @@ export interface AssembleJpegOptions { * @param jpegData - JPEG file data * @returns Object containing EXIF data and position, or null if not found */ -function extractExif (jpegData: Uint8Array): { data: Uint8Array, pos: number, size: number } | null { +function extractExif (jpegData: Uint8Array) { const view = new DataView(jpegData.buffer, jpegData.byteOffset, jpegData.byteLength) // Check for JPEG SOI marker @@ -97,7 +97,7 @@ function extractExif (jpegData: Uint8Array): { data: Uint8Array, pos: number, si * @param exifSize - Size of EXIF segment (including marker and length) * @returns JPEG data without EXIF */ -function copyJpegWithoutExif (jpegData: Uint8Array, exifPos: number, exifSize: number): Uint8Array { +function copyJpegWithoutExif (jpegData: Uint8Array, exifPos: number, exifSize: number) { const newSize = jpegData.length - exifSize const result = new Uint8Array(newSize) @@ -119,7 +119,7 @@ function copyJpegWithoutExif (jpegData: Uint8Array, exifPos: number, exifSize: n * @param data - Data to write after marker * @returns New position after writing */ -function writeMarker (buffer: Uint8Array, pos: number, marker: number, data?: Uint8Array): number { +function writeMarker (buffer: Uint8Array, pos: number, marker: number, data?: Uint8Array) { const view = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength) // Write marker @@ -160,7 +160,7 @@ function writeMarker (buffer: Uint8Array, pos: number, marker: number, data?: Ui * @param options - Assembly options * @returns Complete JPEG-R file as Uint8Array */ -export function assembleJpegWithGainMap (options: AssembleJpegOptions): Uint8Array { +export function assembleJpegWithGainMap (options: AssembleJpegOptions) { const { sdr, gainMap, metadata, exif: externalExif, icc } = options // Validate input diff --git a/src/libultrahdr/mpf-generator.ts b/src/libultrahdr/mpf-generator.ts index 29047eb..b4b9599 100644 --- a/src/libultrahdr/mpf-generator.ts +++ b/src/libultrahdr/mpf-generator.ts @@ -100,12 +100,7 @@ export function calculateMpfSize (): number { * @param secondaryImageOffset - Offset of the secondary image from the MP Endian field * @returns Uint8Array containing the MPF data */ -export function generateMpf ( - primaryImageSize: number, - primaryImageOffset: number, - secondaryImageSize: number, - secondaryImageOffset: number -): Uint8Array { +export function generateMpf (primaryImageSize: number, primaryImageOffset: number, secondaryImageSize: number, secondaryImageOffset: number) { const mpfSize = calculateMpfSize() const buffer = new ArrayBuffer(mpfSize) const view = new DataView(buffer) From ac04fe7981126cb0ba1ad80f95c2b77138c609c8 Mon Sep 17 00:00:00 2001 From: Daniele Pelagatti Date: Wed, 19 Nov 2025 13:03:28 +0100 Subject: [PATCH 9/9] fix build config --- rollup.config.mjs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/rollup.config.mjs b/rollup.config.mjs index 307ac1c..4c646c3 100644 --- a/rollup.config.mjs +++ b/rollup.config.mjs @@ -117,14 +117,7 @@ let configs = [ format: 'es', ...settings }, - plugins: [ - copy({ - targets: [ - { src: 'libultrahdr-wasm/build/libultrahdr-esm.wasm', dest: 'dist' } - ] - }), - ...pluginsMinified - ], + plugins: pluginsMinified, ...configBase }),