@@ -5,6 +5,8 @@ import * as webp from './codecs/webp.ts';
55import * as jxl from './codecs/jxl.ts' ;
66import { availableThreads } from './util/threads.ts' ;
77import { BinaryData , view } from './util/mem.ts' ;
8+ import { GainMap } from './gainmap.ts' ;
9+ import type { ToneMapping } from './codecs/hdr.ts' ;
810import { Image , ResizeMode , ResizeModes , ResizeOptions , ResizeToOptions , resizeArguments } from './image.ts' ;
911import { Frame , FrameDisposalModes } from './frame.ts' ;
1012import { ImageType } from './image-type.ts' ;
@@ -51,6 +53,22 @@ export interface AnimationDecodeOptions {
5153 applyColorProfile ?: boolean ;
5254 /** At most this many worker threads decode in parallel, like `threads` of {@link Image.decode}. Default: the processors available, at most 8 */
5355 threads ?: number ;
56+ /**
57+ * Whether to rotate/mirror the frames as the orientation tag (Exif, JPEG XL's header, PNG's eXIf chunk or XMP)
58+ * says, like `applyOrientation` of {@link Image.decode}: APNGs, animated WebPs and JPEG XLs. Default: `true`
59+ */
60+ applyOrientation ?: boolean ;
61+ /**
62+ * How HDR (PQ or HLG) JPEG XL animations, whose light can be brighter than (SDR) white, become 8 bit sRGB, like
63+ * `toneMapping` of {@link Image.decode}: `'reinhard'` compresses the brightest parts, `'clamp'` clamps everything
64+ * brighter than white. Default: `'reinhard'`
65+ */
66+ toneMapping ?: ToneMapping ;
67+ /**
68+ * Whether to read the gain map (jhgm box) of JPEG XL animations into {@link Image.gainMap} of every frame, like
69+ * `gainMap` of {@link Image.decode}. Default: `true`
70+ */
71+ gainMap ?: boolean ;
5472}
5573
5674/** The options all animation encoders have */
@@ -445,6 +463,8 @@ export class Animation extends Array<Frame> {
445463 const { maxPixels = MAX_PIXELS , threads = 1 } = onlyFirstFrameOrOptions ;
446464 if ( ! Number . isInteger ( threads ) || threads < 1 ) throw new RangeError ( `threads must be a positive integer, not ${ threads } ` ) ;
447465 if ( ! ( Number ( maxPixels ) > 0 ) ) throw new RangeError ( `maxPixels must be a positive number, not ${ maxPixels } ` ) ;
466+ const { toneMapping = 'reinhard' } = onlyFirstFrameOrOptions ;
467+ if ( toneMapping !== 'reinhard' && toneMapping !== 'clamp' ) throw new TypeError ( `toneMapping must be 'reinhard' or 'clamp', not ${ String ( toneMapping ) } ` ) ;
448468 }
449469 return decoding ( ( ) => Animation . __decode__ ( input , onlyFirstFrameOrOptions ) ) ;
450470 }
@@ -454,7 +474,11 @@ export class Animation extends Array<Frame> {
454474 * @ignore
455475 */
456476 private static async __decode__ ( input : BinaryData , onlyFirstFrameOrOptions : boolean | AnimationDecodeOptions ) : Promise < Animation > {
457- const { onlyFirstFrame : onlyExtractFirstFrame = false , maxPixels = MAX_PIXELS , applyColorProfile = true , threads = Math . max ( 1 , Math . min ( availableThreads ( ) , 8 ) ) } = isOptions ( onlyFirstFrameOrOptions ) ? onlyFirstFrameOrOptions : { onlyFirstFrame : onlyFirstFrameOrOptions } ;
477+ const { onlyFirstFrame : onlyExtractFirstFrame = false , maxPixels = MAX_PIXELS , applyColorProfile = true , threads = Math . max ( 1 , Math . min ( availableThreads ( ) , 8 ) ) , applyOrientation = true , toneMapping = 'reinhard' , gainMap = true } = isOptions ( onlyFirstFrameOrOptions ) ? onlyFirstFrameOrOptions : { onlyFirstFrame : onlyFirstFrameOrOptions } ;
478+ const orient = ( frames : Frame [ ] , orientation : number ) : Frame [ ] => {
479+ if ( applyOrientation && orientation > 1 ) for ( const frame of frames ) frame . __orient__ ( orientation ) ;
480+ return frames ;
481+ } ;
458482 const profile = ( frames : Frame [ ] , icc : Uint8Array | undefined , space ?: jxl . ColorSpace ) : Frame [ ] => {
459483 const transform = ! applyColorProfile ? null : icc ? profileTransform ( icc ) : space ? colorSpaceTransform ( space . xy , space . curve ) : null ;
460484 if ( transform ) for ( const frame of frames ) transform ( frame . bitmap ) ;
@@ -467,37 +491,55 @@ export class Animation extends Array<Frame> {
467491
468492 if ( type === 'webp' ) {
469493 const decoded = webp . decodeAnimation ( data , onlyExtractFirstFrame , maxPixels ) ;
470- const frames = profile ( canvasFrames ( decoded . width , decoded . height , webp . composite ( decoded ) ) , decoded . icc ) ;
494+ const frames = orient ( profile ( canvasFrames ( decoded . width , decoded . height , webp . composite ( decoded ) ) , decoded . icc ) , decoded . orientation ) ;
471495 return new Animation ( frames , fromPlays ( decoded . loopCount ) ) ;
472496 }
473497
474- // (still PNGs decode like images, keeping their orientation as it is like before )
498+ // (still PNGs decode like images)
475499 if ( type === 'png' && ! png . isAnimated ( data ) ) {
476- const image = await Image . decode ( data , { maxPixels, applyOrientation : false , applyColorProfile } ) ;
500+ const image = await Image . decode ( data , { maxPixels, applyOrientation, applyColorProfile } ) ;
477501 return new Animation ( canvasFrames ( image . width , image . height , [ { pixels : image . bitmap , duration : 0 } ] ) , - 1 ) ;
478502 }
479503 if ( type === 'png' ) {
480504 const decoded = await png . decodeAnimation ( data , onlyExtractFirstFrame , maxPixels ) ;
481- const frames = profile ( canvasFrames ( decoded . width , decoded . height , png . compositeAnimation ( decoded ) ) , decoded . icc ) ;
505+ const frames = orient ( profile ( canvasFrames ( decoded . width , decoded . height , png . compositeAnimation ( decoded ) ) , decoded . icc ) , decoded . orientation ) ;
482506 return new Animation ( frames , decoded . frames . length > 1 ? fromPlays ( decoded . plays ) : - 1 ) ;
483507 }
484508
485509 if ( type === 'jxl' ) {
486510 const frames : Frame [ ] = [ ] ;
487511 let loopCount = - 1 , icc : Uint8Array | undefined , space : jxl . ColorSpace | undefined ;
488- for await ( const decoded of jxl . frames ( data , maxPixels , 'reinhard' , threads ) ) {
512+ let orientation = 1 ;
513+ for await ( const decoded of jxl . frames ( data , maxPixels , toneMapping , threads ) ) {
489514 const frame = new Frame ( decoded . width , decoded . height , decoded . duration , 0 , 0 , FrameDisposalModes . BACKGROUND ) ;
490515 frame . bitmap = decoded . pixels ;
491- frames . push ( frame . __orient__ ( decoded . orientation ) ) ;
516+ frames . push ( frame ) ;
517+ orientation = decoded . orientation ;
492518 loopCount = fromPlays ( decoded . loopCount ) ;
493519 icc = decoded . icc ;
494520 space = decoded . colorSpace ;
495521 if ( onlyExtractFirstFrame ) break ;
496522 }
497- return new Animation ( profile ( frames , icc , space ) , frames . length > 1 ? loopCount : - 1 ) ;
523+ // (a gain map that can't be read leaves the frames without one, it doesn't fail the decoding)
524+ const map = gainMap && data [ 0 ] !== 0xff ? await jxl . decodeGainMap ( data , maxPixels ) . catch ( ( ) => undefined ) : undefined ;
525+ if ( map ) for ( const frame of frames ) {
526+ const image = new Image ( map . width , map . height ) ;
527+ image . bitmap = map . pixels . slice ( ) ;
528+ frame . gainMap = GainMap . __from_pixels__ ( image , map . params ) ;
529+ }
530+ for ( const frame of orient ( profile ( frames , icc , space ) , orientation ) ) {
531+ const mapOfFrame = frame . gainMap ;
532+ if ( ! mapOfFrame || ! applyOrientation || orientation <= 1 ) continue ;
533+ try {
534+ mapOfFrame . image = mapOfFrame . image . __orient__ ( orientation ) ;
535+ } catch {
536+ frame . gainMap = null ;
537+ }
538+ }
539+ return new Animation ( frames , frames . length > 1 ? loopCount : - 1 ) ;
498540 }
499541
500- if ( type ) return new Animation ( [ Frame . from ( await Image . decode ( data , { maxPixels, applyColorProfile } ) , 0 ) ] ) ;
542+ if ( type ) return new Animation ( [ Frame . from ( await Image . decode ( data , { maxPixels, applyOrientation , applyColorProfile, toneMapping , gainMap , threads } ) , 0 ) ] ) ;
501543 throw new Error ( 'Unsupported image type' ) ;
502544 }
503545
0 commit comments