@@ -20,6 +20,7 @@ import { encodeModular } from './jxl/modular-lossless.ts';
2020import { encodeVarDCT } from './jxl/vardct-enc.ts' ;
2121import { concat } from '../util/mem.ts' ;
2222import { srgbDecode , type Curve } from '../util/icc.ts' ;
23+ import { readISO , writeISO , type Params } from './gainmap.ts' ;
2324
2425const CONTAINER = [ 0 , 0 , 0 , 12 , 0x4a , 0x58 , 0x4c , 0x20 , 0x0d , 0x0a , 0x87 , 0x0a ] ;
2526
@@ -33,15 +34,28 @@ export function isJXL(d: Uint8Array): boolean {
3334 * Encodes RGBA pixels as a JPEG XL, lossless or lossy (with a Butteraugli distance above 0, and an effort 0-2): the
3435 * codestream, in the container if there is Exif data (a TIFF header and IFDs); lossy with up to threads worker threads
3536 */
36- export async function encode ( rgba : Uint8Array , width : number , height : number , exif ?: Uint8Array , distance = 0 , effort = 1 , threads = 1 ) : Promise < Uint8Array > {
37- const stream = distance > 0 ? await encodeVarDCT ( rgba , width , height , { distance, effort, threads } )
38- : effort ? await encodeModular ( rgba , width , height , threads ) : encodeRGBA ( rgba , width , height ) ;
39- if ( ! exif ?. length ) return stream ;
37+ export async function encode ( rgba : Uint8Array , width : number , height : number , exif ?: Uint8Array , distance = 0 , effort = 1 , threads = 1 , gainMap ?: GainMapInput ) : Promise < Uint8Array > {
38+ const codestreamOf = ( pixels : Uint8Array , w : number , h : number , d : number ) : Promise < Uint8Array > | Uint8Array => d > 0 ? encodeVarDCT ( pixels , w , h , { distance : d , effort, threads } )
39+ : effort ? encodeModular ( pixels , w , h , threads ) : encodeRGBA ( pixels , w , h ) ;
40+ const stream = await codestreamOf ( rgba , width , height , distance ) ;
41+ if ( ! exif ?. length && ! gainMap ) return stream ;
4042 // (ftyp: brand jxl, minor version 0, compatible jxl; Exif: the offset of the TIFF header, 0, then the TIFF data)
4143 const jxl = [ 0x6a , 0x78 , 0x6c , 0x20 ] ;
42- return concat ( [ CONTAINER , ...box ( 'ftyp' , jxl , [ 0 , 0 , 0 , 0 ] , jxl ) , ...box ( 'Exif' , [ 0 , 0 , 0 , 0 ] , exif ) , ...box ( 'jxlc' , stream ) ] ) ;
44+ const boxes = [ ...box ( 'ftyp' , jxl , [ 0 , 0 , 0 , 0 ] , jxl ) ] ;
45+ if ( exif ?. length ) boxes . push ( ...box ( 'Exif' , [ 0 , 0 , 0 , 0 ] , exif ) ) ;
46+ if ( gainMap ) {
47+ // jhgm (libjxl's JxlGainMapWriteBundle): version 0, the ISO 21496-1 metadata (its size, 16 bits), no color
48+ // encoding or ICC profile (sizes 0: the alternate image's are the base image's), then the map's codestream
49+ const metadata = writeISO ( gainMap . params ) ;
50+ const map = await codestreamOf ( gainMap . pixels , gainMap . width , gainMap . height , gainMap . distance ) ;
51+ boxes . push ( ...box ( 'jhgm' , [ 0 , metadata . length >> 8 , metadata . length & 255 ] , metadata , [ 0 , 0 , 0 , 0 , 0 ] , map ) ) ;
52+ }
53+ return concat ( [ CONTAINER , ...boxes , ...box ( 'jxlc' , stream ) ] ) ;
4354}
4455
56+ /** A gain map to write: its RGBA pixels, parameters and the distance to encode it at (0: losslessly) */
57+ export interface GainMapInput { pixels : Uint8Array , width : number , height : number , params : Params , distance : number }
58+
4559// a box: its header and content
4660const box = ( type : string , ...content : ArrayLike < number > [ ] ) : ArrayLike < number > [ ] => {
4761 const size = 8 + content . reduce ( ( n , c ) => n + c . length , 0 ) ;
@@ -50,10 +64,9 @@ const box = (type: string, ...content: ArrayLike<number>[]): ArrayLike<number>[]
5064
5165const fourcc = ( d : Uint8Array , o : number ) : string => String . fromCharCode ( d [ o ] , d [ o + 1 ] , d [ o + 2 ] , d [ o + 3 ] ) ;
5266
53- /** The codestream of a file: itself, or the container's jxlc box or jxlp boxes joined */
54- function codestream ( d : Uint8Array ) : Uint8Array {
55- if ( d [ 0 ] === 0xff ) return d ;
56- const view = new DataView ( d . buffer , d . byteOffset , d . byteLength ) , parts : Uint8Array [ ] = [ ] ;
67+ // the container's boxes: their types and contents
68+ function * boxesOf ( d : Uint8Array ) : Generator < { type : string , content : Uint8Array } > {
69+ const view = new DataView ( d . buffer , d . byteOffset , d . byteLength ) ;
5770 for ( let o = 0 ; o + 8 <= d . length ; ) {
5871 let size = view . getUint32 ( o ) , header = 8 ;
5972 const type = fourcc ( d , o + 4 ) ;
@@ -63,11 +76,20 @@ function codestream(d: Uint8Array): Uint8Array {
6376 header = 16 ;
6477 } else if ( size === 0 ) size = d . length - o ;
6578 if ( size < header || o + size > d . length ) throw new Error ( `JPEG XL: invalid ${ type } box` ) ;
66- if ( type === 'jxlc' ) return d . subarray ( o + header , o + size ) ;
67- // (jxlp: a 4-byte index, then a part)
68- if ( type === 'jxlp' ) parts . push ( d . subarray ( o + header + 4 , o + size ) ) ;
79+ yield { type, content : d . subarray ( o + header , o + size ) } ;
6980 o += size ;
7081 }
82+ }
83+
84+ /** The codestream of a file: itself, or the container's jxlc box or jxlp boxes joined */
85+ function codestream ( d : Uint8Array ) : Uint8Array {
86+ if ( d [ 0 ] === 0xff ) return d ;
87+ const parts : Uint8Array [ ] = [ ] ;
88+ for ( const { type, content } of boxesOf ( d ) ) {
89+ if ( type === 'jxlc' ) return content ;
90+ // (jxlp: a 4-byte index, then a part)
91+ if ( type === 'jxlp' ) parts . push ( content . subarray ( 4 ) ) ;
92+ }
7193 if ( ! parts . length ) throw new Error ( 'JPEG XL: no codestream' ) ;
7294 const out = new Uint8Array ( parts . reduce ( ( n , p ) => n + p . length , 0 ) ) ;
7395 let at = 0 ;
@@ -165,8 +187,33 @@ export async function* frames(data: Uint8Array, maxPixels = MAX_PIXELS, toneMapp
165187 }
166188}
167189
168- /** Decodes the first frame shown */
169- export async function decode ( data : Uint8Array , maxPixels = MAX_PIXELS , toneMapping : ToneMapping = 'reinhard' , threads = 1 ) : Promise < { width : number , height : number , pixels : Uint8Array , orientation : number , icc ?: Uint8Array , colorSpace ?: ColorSpace } > {
170- for await ( const frame of frames ( data , maxPixels , toneMapping , threads ) ) return frame ;
190+ /** Decodes the first frame shown (and the gain map of the jhgm box, if asked for and there is one that can be read) */
191+ export async function decode ( data : Uint8Array , maxPixels = MAX_PIXELS , toneMapping : ToneMapping = 'reinhard' , threads = 1 , gainMap = false ) : Promise < { width : number , height : number , pixels : Uint8Array , orientation : number , icc ?: Uint8Array , colorSpace ?: ColorSpace , gainMap ?: { width : number , height : number , pixels : Uint8Array , params : Params } } > {
192+ for await ( const frame of frames ( data , maxPixels , toneMapping , threads ) ) {
193+ if ( ! gainMap || data [ 0 ] === 0xff ) return frame ;
194+ return { ...frame , gainMap : await decodeGainMap ( data , maxPixels ) . catch ( ( ) => undefined ) } ;
195+ }
171196 throw new Error ( 'JPEG XL: no frame shown' ) ;
172197}
198+
199+ // the jhgm box's gain map (libjxl's JxlGainMapReadBundle): version 0, the ISO 21496-1 metadata (its size, 16 bits),
200+ // the alternate image's color encoding and ICC profile (their sizes, 8 and 32 bits), the map's codestream
201+ async function decodeGainMap ( data : Uint8Array , maxPixels : number ) : Promise < { width : number , height : number , pixels : Uint8Array , params : Params } | undefined > {
202+ for ( const { type, content : c } of boxesOf ( data ) ) {
203+ if ( type !== 'jhgm' ) continue ;
204+ if ( c . length < 3 || c [ 0 ] !== 0 ) return undefined ;
205+ const metadataSize = c [ 1 ] << 8 | c [ 2 ] ;
206+ let o = 3 + metadataSize ;
207+ if ( o + 1 > c . length ) return undefined ;
208+ const metadata = c . subarray ( 3 , o ) ;
209+ // (the metadata as ISO 21496-1 has it; or after a version byte, like AVIF's tmap item)
210+ const params = readISO ( metadata ) ?? ( metadata [ 0 ] === 0 ? readISO ( metadata . subarray ( 1 ) ) : null ) ;
211+ o += 1 + c [ o ] ;
212+ if ( ! params || o + 4 > c . length ) return undefined ;
213+ o += 4 + ( c [ o ] << 24 >>> 0 ) + ( c [ o + 1 ] << 16 | c [ o + 2 ] << 8 | c [ o + 3 ] ) ;
214+ if ( o >= c . length ) return undefined ;
215+ // (its values as they are: no color management, no tone mapping)
216+ for await ( const { width, height, pixels } of frames ( c . subarray ( o ) , maxPixels , 'clamp' , 1 ) ) return { width, height, pixels, params } ;
217+ }
218+ return undefined ;
219+ }
0 commit comments