@@ -18,11 +18,12 @@ import {
1818 * Short format brief:
1919 *
2020 * [JPEG headers]
21- * [XMP metadata describing the MPF container and *both* SDR and gainmap images]
21+ * [Metadata describing the MPF container and both SDR and gainmap images]
22+ * - XMP metadata (legacy format)
23+ * - ISO 21496-1 metadata (current standard)
2224 * [Optional metadata] [EXIF] [ICC Profile]
2325 * [SDR image]
24- * [XMP metadata describing only the gainmap image]
25- * [Gainmap image]
26+ * [Gainmap image with metadata]
2627 *
2728 * Each section is separated by a 0xFFXX byte followed by a descriptor byte (0xFFE0, 0xFFE1, 0xFFE2.)
2829 * Binary image storages are prefixed with a unique 0xFFD8 16-bit descriptor.
@@ -45,7 +46,8 @@ for ( let i = 0; i < 1024; i ++ ) {
4546 *
4647 * Current feature set:
4748 * - JPEG headers (required)
48- * - XMP metadata (required)
49+ * - XMP metadata (legacy format, supported)
50+ * - ISO 21496-1 metadata (current standard, supported)
4951 * - XMP validation (not implemented)
5052 * - EXIF profile (not implemented)
5153 * - ICC profile (not implemented)
@@ -109,7 +111,7 @@ class UltraHDRLoader extends Loader {
109111 */
110112 parse ( buffer , onLoad ) {
111113
112- const xmpMetadata = {
114+ const metadata = {
113115 version : null ,
114116 baseRenditionIsHDR : null ,
115117 gainMapMin : null ,
@@ -197,18 +199,47 @@ class UltraHDRLoader extends Loader {
197199 /* JPEG Header - no useful information */
198200 } else if ( sectionType === 0xe1 ) {
199201
200- /* XMP Metadata */
202+ /* APP1: XMP Metadata */
201203
202204 this . _parseXMPMetadata (
203205 textDecoder . decode ( new Uint8Array ( section ) ) ,
204- xmpMetadata
206+ metadata
205207 ) ;
206208
207209 } else if ( sectionType === 0xe2 ) {
208210
209- /* Data Sections - MPF / EXIF / ICC Profile */
211+ /* APP2: Data Sections - MPF / ICC Profile / ISO 21496-1 Metadata */
210212
211213 const sectionData = new DataView ( section . buffer , section . byteOffset + 2 , section . byteLength - 2 ) ;
214+
215+ // Check for ISO 21496-1 namespace: "urn:iso:std:iso:ts:21496:-1\0"
216+ const isoNameSpace = 'urn:iso:std:iso:ts:21496:-1\0' ;
217+ if ( section . byteLength >= isoNameSpace . length + 2 ) {
218+
219+ let isISO = true ;
220+ for ( let j = 0 ; j < isoNameSpace . length ; j ++ ) {
221+
222+ if ( section [ 2 + j ] !== isoNameSpace . charCodeAt ( j ) ) {
223+
224+ isISO = false ;
225+ break ;
226+
227+ }
228+
229+ }
230+
231+ if ( isISO ) {
232+
233+ // Parse ISO 21496-1 metadata
234+ const isoData = section . subarray ( 2 + isoNameSpace . length ) ;
235+ this . _parseISOMetadata ( isoData , metadata ) ;
236+ continue ;
237+
238+ }
239+
240+ }
241+
242+ // Check for MPF
212243 const sectionHeader = sectionData . getUint32 ( 2 , false ) ;
213244
214245 if ( sectionHeader === 0x4d504600 ) {
@@ -277,7 +308,8 @@ class UltraHDRLoader extends Loader {
277308 }
278309
279310 /* Minimal sufficient validation - https://developer.android.com/media/platform/hdr-image-format#signal_of_the_format */
280- if ( ! xmpMetadata . version ) {
311+ // Version can come from either XMP or ISO metadata
312+ if ( ! metadata . version ) {
281313
282314 throw new Error ( 'THREE.UltraHDRLoader: Not a valid UltraHDR image' ) ;
283315
@@ -286,7 +318,7 @@ class UltraHDRLoader extends Loader {
286318 if ( primaryImage && gainmapImage ) {
287319
288320 this . _applyGainmapToSDR (
289- xmpMetadata ,
321+ metadata ,
290322 primaryImage ,
291323 gainmapImage ,
292324 ( hdrBuffer , width , height ) => {
@@ -315,6 +347,126 @@ class UltraHDRLoader extends Loader {
315347
316348 }
317349
350+ /**
351+ * Parses ISO 21496-1 gainmap metadata from binary data.
352+ *
353+ * @private
354+ * @param {Uint8Array } data - The binary ISO metadata.
355+ * @param {Object } metadata - The metadata object to populate.
356+ */
357+ _parseISOMetadata ( data , metadata ) {
358+
359+ const view = new DataView ( data . buffer , data . byteOffset , data . byteLength ) ;
360+
361+ // Skip minimum version (2 bytes) and writer version (2 bytes)
362+ let offset = 4 ;
363+
364+ // Read flags (1 byte)
365+ const flags = view . getUint8 ( offset ) ;
366+ offset += 1 ;
367+
368+ const backwardDirection = ( flags & 0x4 ) !== 0 ;
369+ const useCommonDenominator = ( flags & 0x8 ) !== 0 ;
370+
371+ let gainMapMin , gainMapMax , gamma , offsetSDR , offsetHDR , hdrCapacityMin , hdrCapacityMax ;
372+
373+ if ( useCommonDenominator ) {
374+
375+ // Read common denominator (4 bytes, unsigned)
376+ const commonDenominator = view . getUint32 ( offset , false ) ;
377+ offset += 4 ;
378+
379+ // Read baseHdrHeadroom (4 bytes, unsigned)
380+ const baseHdrHeadroomN = view . getUint32 ( offset , false ) ;
381+ offset += 4 ;
382+ hdrCapacityMin = Math . log2 ( baseHdrHeadroomN / commonDenominator ) ;
383+
384+ // Read alternateHdrHeadroom (4 bytes, unsigned)
385+ const alternateHdrHeadroomN = view . getUint32 ( offset , false ) ;
386+ offset += 4 ;
387+ hdrCapacityMax = Math . log2 ( alternateHdrHeadroomN / commonDenominator ) ;
388+
389+ // Read first channel (or only channel) parameters
390+ const gainMapMinN = view . getInt32 ( offset , false ) ;
391+ offset += 4 ;
392+ gainMapMin = gainMapMinN / commonDenominator ;
393+
394+ const gainMapMaxN = view . getInt32 ( offset , false ) ;
395+ offset += 4 ;
396+ gainMapMax = gainMapMaxN / commonDenominator ;
397+
398+ const gammaN = view . getUint32 ( offset , false ) ;
399+ offset += 4 ;
400+ gamma = gammaN / commonDenominator ;
401+
402+ const offsetSDRN = view . getInt32 ( offset , false ) ;
403+ offset += 4 ;
404+ offsetSDR = ( offsetSDRN / commonDenominator ) * 255.0 ;
405+
406+ const offsetHDRN = view . getInt32 ( offset , false ) ;
407+ offsetHDR = ( offsetHDRN / commonDenominator ) * 255.0 ;
408+
409+ } else {
410+
411+ // Read baseHdrHeadroom numerator and denominator
412+ const baseHdrHeadroomN = view . getUint32 ( offset , false ) ;
413+ offset += 4 ;
414+ const baseHdrHeadroomD = view . getUint32 ( offset , false ) ;
415+ offset += 4 ;
416+ hdrCapacityMin = Math . log2 ( baseHdrHeadroomN / baseHdrHeadroomD ) ;
417+
418+ // Read alternateHdrHeadroom numerator and denominator
419+ const alternateHdrHeadroomN = view . getUint32 ( offset , false ) ;
420+ offset += 4 ;
421+ const alternateHdrHeadroomD = view . getUint32 ( offset , false ) ;
422+ offset += 4 ;
423+ hdrCapacityMax = Math . log2 ( alternateHdrHeadroomN / alternateHdrHeadroomD ) ;
424+
425+ // Read first channel parameters
426+ const gainMapMinN = view . getInt32 ( offset , false ) ;
427+ offset += 4 ;
428+ const gainMapMinD = view . getUint32 ( offset , false ) ;
429+ offset += 4 ;
430+ gainMapMin = gainMapMinN / gainMapMinD ;
431+
432+ const gainMapMaxN = view . getInt32 ( offset , false ) ;
433+ offset += 4 ;
434+ const gainMapMaxD = view . getUint32 ( offset , false ) ;
435+ offset += 4 ;
436+ gainMapMax = gainMapMaxN / gainMapMaxD ;
437+
438+ const gammaN = view . getUint32 ( offset , false ) ;
439+ offset += 4 ;
440+ const gammaD = view . getUint32 ( offset , false ) ;
441+ offset += 4 ;
442+ gamma = gammaN / gammaD ;
443+
444+ const offsetSDRN = view . getInt32 ( offset , false ) ;
445+ offset += 4 ;
446+ const offsetSDRD = view . getUint32 ( offset , false ) ;
447+ offset += 4 ;
448+ offsetSDR = ( offsetSDRN / offsetSDRD ) * 255.0 ;
449+
450+ const offsetHDRN = view . getInt32 ( offset , false ) ;
451+ offset += 4 ;
452+ const offsetHDRD = view . getUint32 ( offset , false ) ;
453+ offsetHDR = ( offsetHDRN / offsetHDRD ) * 255.0 ;
454+
455+ }
456+
457+ // Convert log2 values to linear
458+ metadata . version = '1.0' ; // ISO standard doesn't encode version string, use default
459+ metadata . baseRenditionIsHDR = backwardDirection ;
460+ metadata . gainMapMin = gainMapMin ;
461+ metadata . gainMapMax = gainMapMax ;
462+ metadata . gamma = gamma ;
463+ metadata . offsetSDR = offsetSDR ;
464+ metadata . offsetHDR = offsetHDR ;
465+ metadata . hdrCapacityMin = hdrCapacityMin ;
466+ metadata . hdrCapacityMax = hdrCapacityMax ;
467+
468+ }
469+
318470 /**
319471 * Starts loading from the given URL and passes the loaded Ultra HDR texture
320472 * to the `onLoad()` callback.
@@ -383,7 +535,7 @@ class UltraHDRLoader extends Loader {
383535
384536 }
385537
386- _parseXMPMetadata ( xmpDataString , xmpMetadata ) {
538+ _parseXMPMetadata ( xmpDataString , metadata ) {
387539
388540 const domParser = new DOMParser ( ) ;
389541
@@ -408,28 +560,28 @@ class UltraHDRLoader extends Loader {
408560
409561 const [ gainmapNode ] = xmpXml . getElementsByTagName ( 'rdf:Description' ) ;
410562
411- xmpMetadata . version = gainmapNode . getAttribute ( 'hdrgm:Version' ) ;
412- xmpMetadata . baseRenditionIsHDR =
563+ metadata . version = gainmapNode . getAttribute ( 'hdrgm:Version' ) ;
564+ metadata . baseRenditionIsHDR =
413565 gainmapNode . getAttribute ( 'hdrgm:BaseRenditionIsHDR' ) === 'True' ;
414- xmpMetadata . gainMapMin = parseFloat (
566+ metadata . gainMapMin = parseFloat (
415567 gainmapNode . getAttribute ( 'hdrgm:GainMapMin' ) || 0.0
416568 ) ;
417- xmpMetadata . gainMapMax = parseFloat (
569+ metadata . gainMapMax = parseFloat (
418570 gainmapNode . getAttribute ( 'hdrgm:GainMapMax' ) || 1.0
419571 ) ;
420- xmpMetadata . gamma = parseFloat (
572+ metadata . gamma = parseFloat (
421573 gainmapNode . getAttribute ( 'hdrgm:Gamma' ) || 1.0
422574 ) ;
423- xmpMetadata . offsetSDR = parseFloat (
575+ metadata . offsetSDR = parseFloat (
424576 gainmapNode . getAttribute ( 'hdrgm:OffsetSDR' ) / ( 1 / 64 )
425577 ) ;
426- xmpMetadata . offsetHDR = parseFloat (
578+ metadata . offsetHDR = parseFloat (
427579 gainmapNode . getAttribute ( 'hdrgm:OffsetHDR' ) / ( 1 / 64 )
428580 ) ;
429- xmpMetadata . hdrCapacityMin = parseFloat (
581+ metadata . hdrCapacityMin = parseFloat (
430582 gainmapNode . getAttribute ( 'hdrgm:HDRCapacityMin' ) || 0.0
431583 ) ;
432- xmpMetadata . hdrCapacityMax = parseFloat (
584+ metadata . hdrCapacityMax = parseFloat (
433585 gainmapNode . getAttribute ( 'hdrgm:HDRCapacityMax' ) || 1.0
434586 ) ;
435587
@@ -459,7 +611,7 @@ class UltraHDRLoader extends Loader {
459611 }
460612
461613 _applyGainmapToSDR (
462- xmpMetadata ,
614+ metadata ,
463615 sdrBuffer ,
464616 gainmapBuffer ,
465617 onSuccess ,
@@ -527,10 +679,10 @@ class UltraHDRLoader extends Loader {
527679 /* HDR Recovery formula - https://developer.android.com/media/platform/hdr-image-format#use_the_gain_map_to_create_adapted_HDR_rendition */
528680
529681 /* 1.8 instead of 2 near-perfectly rectifies approximations introduced by precalculated SRGB_TO_LINEAR values */
530- const maxDisplayBoost = 1.8 ** ( xmpMetadata . hdrCapacityMax * 0.5 ) ;
682+ const maxDisplayBoost = 1.8 ** ( metadata . hdrCapacityMax * 0.5 ) ;
531683 const unclampedWeightFactor =
532- ( Math . log2 ( maxDisplayBoost ) - xmpMetadata . hdrCapacityMin ) /
533- ( xmpMetadata . hdrCapacityMax - xmpMetadata . hdrCapacityMin ) ;
684+ ( Math . log2 ( maxDisplayBoost ) - metadata . hdrCapacityMin ) /
685+ ( metadata . hdrCapacityMax - metadata . hdrCapacityMin ) ;
534686 const weightFactor = Math . min (
535687 Math . max ( unclampedWeightFactor , 0.0 ) ,
536688 1.0
@@ -539,12 +691,12 @@ class UltraHDRLoader extends Loader {
539691 const sdrData = sdrImageData . data ;
540692 const gainmapData = gainmapImageData . data ;
541693 const dataLength = sdrData . length ;
542- const gainMapMin = xmpMetadata . gainMapMin ;
543- const gainMapMax = xmpMetadata . gainMapMax ;
544- const offsetSDR = xmpMetadata . offsetSDR ;
545- const offsetHDR = xmpMetadata . offsetHDR ;
546- const invGamma = 1.0 / xmpMetadata . gamma ;
547- const useGammaOne = xmpMetadata . gamma === 1.0 ;
694+ const gainMapMin = metadata . gainMapMin ;
695+ const gainMapMax = metadata . gainMapMax ;
696+ const offsetSDR = metadata . offsetSDR ;
697+ const offsetHDR = metadata . offsetHDR ;
698+ const invGamma = 1.0 / metadata . gamma ;
699+ const useGammaOne = metadata . gamma === 1.0 ;
548700 const isHalfFloat = this . type === HalfFloatType ;
549701 const toHalfFloat = DataUtils . toHalfFloat ;
550702 const srgbToLinear = this . _srgbToLinear ;
0 commit comments