@@ -36,8 +36,7 @@ const perChannel = (value: number | number[], name: string): number[] => {
3636 * (JPEG-R) photos from phones carry it. HDR displays show the image boosted by it, the others the image as it is.
3737 *
3838 * {@link Image.decode} reads the gain maps of JPEGs, AVIFs and JPEG XLs into {@link Image.gainMap},
39- * {@link Image.encodeJPEG}, {@link Image.encodeAVIF} and {@link Image.encodeJXL} write them;
40- * the map's pixels are only decoded once they're needed. The map covers the whole image, at any resolution (phones
39+ * {@link Image.encodeJPEG}, {@link Image.encodeAVIF} and {@link Image.encodeJXL} write them. The map covers the whole image, at any resolution (phones
4140 * write a quarter of the width and height). Its value `v` (0-255, per channel or one for all) boosts the linear
4241 * light of the image by `2^(gainMapMin + (gainMapMax - gainMapMin) * (v / 255)^(1 / gamma))`
4342 */
@@ -59,12 +58,8 @@ export class GainMap {
5958 /** whether the boost applies in the SDR image's color space (else in the HDR image's) */
6059 useBaseColorSpace : boolean ;
6160
62- /** the map's JPEG (without metadata) until its pixels are used */
63- private __jpeg__ : Uint8Array | null = null ;
64- /** its pixels from then on */
61+ /** its pixels */
6562 private __pixels__ : Image | null = null ;
66- /** an Apple gain map's headroom: its values are converted when decoded (and the JPEG can't be kept as it is) */
67- private __apple__ = 0 ;
6863
6964 /**
7065 * Creates a gain map
@@ -87,21 +82,38 @@ export class GainMap {
8782 this . __check__ ( ) ;
8883 }
8984
90- /** @internal from a decoded file: the map's JPEG, its pixels decoded when first used */
85+ /**
86+ * @internal from a decoded file: the map's JPEG, decoded now (a damaged one throws here, not in a later
87+ * operation); `apple`: an Apple gain map's headroom, its values converted to the parameters' scale
88+ */
9189 static __from_jpeg__ ( map : Uint8Array , params : codec . Params , apple = 0 ) : GainMap {
92- const gainMap = Object . create ( GainMap . prototype ) as GainMap ;
93- gainMap . __pixels__ = null ;
94- gainMap . __apple__ = apple ;
95- gainMap . __jpeg__ = codec . stripMetadata ( map ) ;
96- gainMap . __set_params__ ( params ) ;
97- return gainMap ;
90+ const { width, height, pixels } = jpeg . decode ( map ) ;
91+ const image = new Image ( width , height ) ;
92+ image . bitmap = pixels ;
93+ if ( apple ) {
94+ const table = new Uint8Array ( 256 ) ;
95+ for ( let v = 0 ; v < 256 ; v ++ ) table [ v ] = Math . round ( 255 * codec . appleValue ( v / 255 , apple ) ) ;
96+ for ( let i = 0 ; i < pixels . length ; i += 4 ) {
97+ pixels [ i ] = table [ pixels [ i ] ] ;
98+ pixels [ i + 1 ] = table [ pixels [ i + 1 ] ] ;
99+ pixels [ i + 2 ] = table [ pixels [ i + 2 ] ] ;
100+ }
101+ }
102+ return GainMap . __from_pixels__ ( image , params , false ) ;
98103 }
99104
100- /** @internal from decoded pixels (an AVIF's or JPEG XL's map) */
101- static __from_pixels__ ( image : Image , params : codec . Params ) : GainMap {
105+ /**
106+ * @internal from decoded pixels (an AVIF's or JPEG XL's map); `grey`: a map whose channels differ by at most 2
107+ * levels anywhere is made grey (a grey map stored in color, like libjxl writes them: one channel when written)
108+ */
109+ static __from_pixels__ ( image : Image , params : codec . Params , grey = true ) : GainMap {
110+ const px = image . bitmap ;
111+ if ( grey ) {
112+ let spread = 0 ;
113+ for ( let i = 0 ; i < px . length && spread <= 2 ; i += 4 ) spread = Math . max ( spread , Math . abs ( px [ i ] - px [ i + 1 ] ) , Math . abs ( px [ i ] - px [ i + 2 ] ) ) ;
114+ if ( spread > 0 && spread <= 2 ) for ( let i = 0 ; i < px . length ; i += 4 ) px [ i ] = px [ i + 1 ] = px [ i + 2 ] = Math . round ( ( px [ i ] + px [ i + 1 ] + px [ i + 2 ] ) / 3 ) ;
115+ }
102116 const gainMap = Object . create ( GainMap . prototype ) as GainMap ;
103- gainMap . __jpeg__ = null ;
104- gainMap . __apple__ = 0 ;
105117 gainMap . __pixels__ = image ;
106118 gainMap . __set_params__ ( params ) ;
107119 return gainMap ;
@@ -140,50 +152,29 @@ export class GainMap {
140152 if ( p . capacityMin < 0 || p . capacityMax <= p . capacityMin ) throw new RangeError ( 'The gain map\'s hdrCapacityMax must be above its hdrCapacityMin, which can\'t be negative' ) ;
141153 }
142154
143- /** The map's pixels (decoded when first read) . Changes to them are written when the image is encoded */
155+ /** The map's pixels. Changes to them are written when the image is encoded */
144156 get image ( ) : Image {
145- if ( ! this . __pixels__ ) {
146- const { width, height, pixels } = jpeg . decode ( this . __jpeg__ as Uint8Array ) ;
147- const image = new Image ( width , height ) ;
148- image . bitmap = pixels ;
149- if ( this . __apple__ ) {
150- // (Apple's values to the parameters' log2 scale)
151- const table = new Uint8Array ( 256 ) ;
152- for ( let v = 0 ; v < 256 ; v ++ ) table [ v ] = Math . round ( 255 * codec . appleValue ( v / 255 , this . __apple__ ) ) ;
153- for ( let i = 0 ; i < pixels . length ; i += 4 ) {
154- pixels [ i ] = table [ pixels [ i ] ] ;
155- pixels [ i + 1 ] = table [ pixels [ i + 1 ] ] ;
156- pixels [ i + 2 ] = table [ pixels [ i + 2 ] ] ;
157- }
158- this . __apple__ = 0 ;
159- }
160- this . __pixels__ = image ;
161- // (from now on the pixels can change: encoding writes them anew)
162- this . __jpeg__ = null ;
163- }
164- return this . __pixels__ ;
157+ return this . __pixels__ as Image ;
165158 }
166159
167160 set image ( image : Image ) {
168161 if ( ! ( image instanceof Image ) ) throw new TypeError ( 'The gain map has to be an Image' ) ;
169162 this . __pixels__ = image ;
170- this . __jpeg__ = null ;
171163 }
172164
173- /** The map's width (without decoding it) */
165+ /** The map's width */
174166 get width ( ) : number {
175- return this . __pixels__ ? this . __pixels__ . width : codec . dimensions ( this . __jpeg__ as Uint8Array ) ? .width ?? 0 ;
167+ return ( this . __pixels__ as Image ) . width ;
176168 }
177169
178- /** The map's height (without decoding it) */
170+ /** The map's height */
179171 get height ( ) : number {
180- return this . __pixels__ ? this . __pixels__ . height : codec . dimensions ( this . __jpeg__ as Uint8Array ) ? .height ?? 0 ;
172+ return ( this . __pixels__ as Image ) . height ;
181173 }
182174
183175 /** `1` for a grey map (one boost for red, green and blue), `3` for one per channel */
184176 get channels ( ) : 1 | 3 {
185- if ( ! this . __pixels__ ) return codec . componentCount ( this . __jpeg__ as Uint8Array ) === 1 ? 1 : 3 ;
186- const bitmap = this . __pixels__ . bitmap ;
177+ const bitmap = ( this . __pixels__ as Image ) . bitmap ;
187178 for ( let i = 0 ; i < bitmap . length ; i += 4 ) if ( bitmap [ i ] !== bitmap [ i + 1 ] || bitmap [ i ] !== bitmap [ i + 2 ] ) return 3 ;
188179 return 1 ;
189180 }
@@ -192,18 +183,14 @@ export class GainMap {
192183 clone ( ) : GainMap {
193184 const copy = Object . create ( GainMap . prototype ) as GainMap ;
194185 copy . __set_params__ ( this . __params__ ( ) ) ;
195- copy . __jpeg__ = this . __jpeg__ ;
196- copy . __apple__ = this . __apple__ ;
197- copy . __pixels__ = this . __pixels__ ? this . __pixels__ . clone ( ) : null ;
186+ copy . __pixels__ = ( this . __pixels__ as Image ) . clone ( ) ;
198187 return copy ;
199188 }
200189
201190 /**
202- * @internal the map's JPEG without metadata: the decoded file's as it is if the pixels weren't used and no quality
203- * or size is asked for, else encoded anew
191+ * @internal the map as a JPEG without metadata, at a quality (default 90) and size
204192 */
205193 __body__ ( quality ?: number , width ?: number , height ?: number ) : Uint8Array {
206- if ( this . __jpeg__ && ! this . __apple__ && quality === undefined && ( width === undefined || width === this . width && height === this . height ) ) return this . __jpeg__ ;
207194 let image = this . image ;
208195 if ( width !== undefined && height !== undefined && ( width !== image . width || height !== image . height ) ) image = image . clone ( ) . resize ( width , height ) ;
209196 const bitmap = image . bitmap ;
@@ -270,8 +257,7 @@ export class GainMap {
270257 /**
271258 * Encodes the gain map alone: a JPEG with its parameters (hdrgm XMP and ISO 21496-1), like the second image in an
272259 * Ultra HDR JPEG
273- * @param quality The JPEG quality (1-100). Default: `90`, or the decoded file's map as it is if its pixels weren't
274- * used
260+ * @param quality The JPEG quality (1-100). Default: `90`
275261 */
276262 async encode ( quality ?: number ) : Promise < Uint8Array > {
277263 if ( quality !== undefined ) quality = Math . max ( 1 , checkQuality ( quality ) ) ;
@@ -289,9 +275,6 @@ export class GainMap {
289275 if ( found ) return GainMap . __from_jpeg__ ( found . map , found . params , found . apple ) ;
290276 const params = codec . readParams ( bytes ) ;
291277 if ( ! params ) throw new Error ( 'No gain map (or no gain map parameters) found' ) ;
292- const gainMap = GainMap . __from_jpeg__ ( bytes , params ) ;
293- // (decoded now: a damaged JPEG throws here, not later)
294- void gainMap . image ;
295- return gainMap ;
278+ return GainMap . __from_jpeg__ ( bytes , params ) ;
296279 }
297280}
0 commit comments