From 1a94472af69cf06395e5591ee66b4a6a4729200f Mon Sep 17 00:00:00 2001 From: Arecsu Date: Fri, 29 Nov 2024 08:28:36 -0300 Subject: [PATCH] remove DOMParser(), decode compatible with Web Workers features like Offscreen Canvas use Web Workers. If we want to process a gainmap embedded in a JPEG, we won't have access to DOMParser() and thus need a compatible alternative. This is a simple Regex approach to get around it. --- src/decode/utils/extractXMP.ts | 68 +++++++++------------- src/libultrahdr/decode-jpeg-metadata.ts | 75 ++++++++++--------------- 2 files changed, 58 insertions(+), 85 deletions(-) diff --git a/src/decode/utils/extractXMP.ts b/src/decode/utils/extractXMP.ts index 8e1180e..c48254c 100644 --- a/src/decode/utils/extractXMP.ts +++ b/src/decode/utils/extractXMP.ts @@ -1,33 +1,25 @@ import { GainMapMetadata } from '../../core/types' -const getAttribute = (description: Element, name: string, defaultValue?: string) => { - let returnValue: string | [string, string, string] - const parsedValue = description.attributes.getNamedItem(name)?.nodeValue - if (!parsedValue) { - const node = description.getElementsByTagName(name)[0] - if (node) { - const values = node.getElementsByTagName('rdf:li') - if (values.length === 3) { - returnValue = Array.from(values).map(v => v.innerHTML) as [string, string, string] - } else { - throw new Error(`Gainmap metadata contains an array of items for ${name} but its length is not 3`) - } - } else { - if (defaultValue) return defaultValue - else throw new Error(`Can't find ${name} in gainmap metadata`) +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] } - } else { - returnValue = parsedValue + return tagMatch[1].trim() } - return returnValue + if (defaultValue !== undefined) return defaultValue + throw new Error(`Can't find ${tag} in gainmap metadata`) } -/** - * - * @param input - * @returns - */ export const extractXMP = (input: Uint8Array): GainMapMetadata | undefined => { let str: string // support node test environment @@ -35,29 +27,25 @@ export const extractXMP = (input: Uint8Array): GainMapMetadata | undefined => { else str = input.toString() let start = str.indexOf('', start) - str.slice(start, end + 10) const xmpBlock = str.slice(start, end + 10) - try { - const xmlDocument = parser.parseFromString(xmpBlock, 'text/xml') - const description = xmlDocument.getElementsByTagName('rdf:Description')[0] - - const gainMapMin = getAttribute(description, 'hdrgm:GainMapMin', '0') - const gainMapMax = getAttribute(description, 'hdrgm:GainMapMax') - const gamma = getAttribute(description, 'hdrgm:Gamma', '1') - - const offsetSDR = getAttribute(description, 'hdrgm:OffsetSDR', '0.015625') - const offsetHDR = getAttribute(description, 'hdrgm:OffsetHDR', '0.015625') + try { + const gainMapMin = getXMLValue(xmpBlock, 'hdrgm:GainMapMin', '0') + const gainMapMax = getXMLValue(xmpBlock, 'hdrgm:GainMapMax') + const gamma = getXMLValue(xmpBlock, 'hdrgm:Gamma', '1') + const offsetSDR = getXMLValue(xmpBlock, 'hdrgm:OffsetSDR', '0.015625') + const offsetHDR = getXMLValue(xmpBlock, 'hdrgm:OffsetHDR', '0.015625') - let hdrCapacityMin = description.attributes.getNamedItem('hdrgm:HDRCapacityMin')?.nodeValue - if (!hdrCapacityMin) hdrCapacityMin = '0' + // These are always attributes, so we can use a simpler regex + const hdrCapacityMinMatch = /hdrgm:HDRCapacityMin="([^"]*)"/.exec(xmpBlock) + const hdrCapacityMin = hdrCapacityMinMatch ? hdrCapacityMinMatch[1] : '0' - const hdrCapacityMax = description.attributes.getNamedItem('hdrgm:HDRCapacityMax')?.nodeValue - if (!hdrCapacityMax) throw new Error('Incomplete gainmap metadata') + const hdrCapacityMaxMatch = /hdrgm:HDRCapacityMax="([^"]*)"/.exec(xmpBlock) + if (!hdrCapacityMaxMatch) throw new Error('Incomplete gainmap metadata') + const hdrCapacityMax = hdrCapacityMaxMatch[1] return { gainMapMin: Array.isArray(gainMapMin) ? gainMapMin.map(v => parseFloat(v)) as [number, number, number] : [parseFloat(gainMapMin), parseFloat(gainMapMin), parseFloat(gainMapMin)], @@ -69,7 +57,7 @@ export const extractXMP = (input: Uint8Array): GainMapMetadata | undefined => { hdrCapacityMax: parseFloat(hdrCapacityMax) } } catch (e) { - + // Continue searching for another xmpmeta block if this one fails } start = str.indexOf(' { - let returnValue: string | [string, string, string] - const parsedValue = description.attributes.getNamedItem(name)?.nodeValue - if (!parsedValue) { - const node = description.getElementsByTagName(name)[0] - if (node) { - const values = node.getElementsByTagName('rdf:li') - if (values.length === 3) { - returnValue = Array.from(values).map(v => v.innerHTML) as [string, string, string] - } else { - throw new Error(`Gainmap metadata contains an array of items for ${name} but its length is not 3`) - } - } else { - if (defaultValue) return defaultValue - else throw new Error(`Can't find ${name} in gainmap metadata`) - } - } else { - returnValue = parsedValue - } - - return returnValue -} /** * Decodes a JPEG file with an embedded Gainmap and XMP Metadata (aka JPEG-R) * @@ -56,23 +26,41 @@ export const decodeJPEGMetadata = async (file: Uint8Array) => { const result = lib.extractJpegR(file, file.length) if (!result.success) throw new Error(`${result.errorMessage}`) - const parser = new DOMParser() - const xmlDocument = parser.parseFromString(result.metadata as string, 'text/xml') - const description = xmlDocument.getElementsByTagName('rdf:Description')[0] + 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() + } - const gainMapMin = getAttribute(description, 'hdrgm:GainMapMin', '0') - const gainMapMax = getAttribute(description, 'hdrgm:GainMapMax') + if (defaultValue !== undefined) return defaultValue + throw new Error(`Can't find ${tag} in gainmap metadata`) + } - const gamma = getAttribute(description, 'hdrgm:Gamma', '1') + const metadata = result.metadata as string - const offsetSDR = getAttribute(description, 'hdrgm:OffsetSDR', '0.015625') - const offsetHDR = getAttribute(description, 'hdrgm:OffsetHDR', '0.015625') + 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') - let hdrCapacityMin = description.attributes.getNamedItem('hdrgm:HDRCapacityMin')?.nodeValue - if (!hdrCapacityMin) hdrCapacityMin = '0' + // These are always attributes, so we can use a simpler regex + const hdrCapacityMinMatch = /hdrgm:HDRCapacityMin="([^"]*)"/.exec(metadata) + const hdrCapacityMin = hdrCapacityMinMatch ? hdrCapacityMinMatch[1] : '0' - const hdrCapacityMax = description.attributes.getNamedItem('hdrgm:HDRCapacityMax')?.nodeValue - if (!hdrCapacityMax) throw new Error('Incomplete gainmap metadata') + 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)], @@ -86,9 +74,6 @@ export const decodeJPEGMetadata = async (file: Uint8Array) => { return { ...result, - /** - * Parsed metadata - */ parsedMetadata } }