@@ -10,13 +10,14 @@ import {
1010 FrameHeader , TOC , frameHeader , toc , passShifts , FRAME_LF , FRAME_REGULAR , FRAME_SKIP_PROGRESSIVE , BLEND_REPLACE , BLEND_ADD ,
1111 BLEND_BLEND , BLEND_MULADD , BLEND_MUL , FLAG_PATCHES , FLAG_SPLINES , FLAG_NOISE , FLAG_USE_LF_FRAME , type EPF
1212} from './frame-header.ts' ;
13- import { ModularImage , Channel , MAConfig , channel , readTree , decodeModular } from './modular.ts' ;
13+ import { ModularImage , Channel , MAConfig , TR_RCT , channel , readTree , decodeModular , rctOrder , rctSamples } from './modular.ts' ;
1414import { upsample } from './upsample.ts' ;
1515import { gaborish , gaborishRows , epf , epfStrip , epfScratch , epfSteps , epfStrips } from './filters.ts' ;
1616import { VarDCT , VarDCTGlobal , readVarDCTGlobal , type VarDCTShared } from './vardct.ts' ;
1717import { runChunks , shared , sharedOf , type ChunkTask , type Thread } from '../../util/threads.ts' ;
1818import { Patch , Splines , readPatches , applyPatches , readSplines , renderSplines , readNoise , renderNoise } from './features.ts' ;
19- import { ColorOutput , xybToBytes } from './color.ts' ;
19+ import { byte , ColorOutput , xybToBytes } from './color.ts' ;
20+ import { littleEndian } from '../../util/alpha.ts' ;
2021import { Blending , performBlending , PB_REPLACE , PB_ADD , PB_BLEND_ABOVE , PB_ADD_ABOVE , PB_MUL } from './blend.ts' ;
2122
2223/** Float planes: the 3 color channels (or 1), then the extra channels */
@@ -149,8 +150,15 @@ export async function decodeFrame(d: Uint8Array, r: BitReader, state: DecoderSta
149150 }
150151 }
151152 if ( ! groupsDone ) for ( let g = 0 ; g < f . numGroups ; g ++ ) local ( g ) ;
152- lf . modular . undo ( ) ;
153- let planes = modularToPlanes ( lf , f , image ) ;
153+ // (a lossless frame shown as it is: its samples to bytes right away)
154+ const bytes = ! vardct && f . modular && ! f . doYCbCr && ! f . gab && ! f . epf && ! lf . patches && ! lf . splines && ! lf . noise && f . upsampling === 1
155+ && f . ecUpsampling . every ( u => u === 1 ) && shownAsIs ( f , image ) && state . output . bytesOfSamples ( ) && ! image . bitDepth . float ;
156+ let planes : Planes ;
157+ if ( bytes ) planes = await modularToBytes ( lf , f , image , pool ) ;
158+ else {
159+ lf . modular . undo ( ) ;
160+ planes = modularToPlanes ( lf , f , image ) ;
161+ }
154162 let sigma : Float32Array | null = null ;
155163 if ( vardct ) {
156164 const xyb = vardct . rendered ;
@@ -313,18 +321,21 @@ function decodeModularGroup(r: BitReader, lf: LfGlobal, f: FrameHeader, x0: numb
313321}
314322
315323// a Modular image's channels as floats: [0, 1] for integers, cast for float samples; XYB scaled by the LF dequantization
324+ // a channel's samples as floats (integers scaled to 0 to 1)
325+ function samplesToFloat ( c : Channel , depth : ImageHeader [ 'bitDepth' ] ) : Float32Array {
326+ const o = new Float32Array ( c . w * c . h ) ;
327+ if ( depth . float ) for ( let i = 0 ; i < o . length ; i ++ ) o [ i ] = floatSample ( c . data [ i ] , depth . bits , depth . expBits ) ;
328+ else {
329+ const scale = 1 / ( 2 ** depth . bits - 1 ) ;
330+ for ( let i = 0 ; i < o . length ; i ++ ) o [ i ] = c . data [ i ] * scale ;
331+ }
332+ return o ;
333+ }
334+
316335function modularToPlanes ( lf : LfGlobal , f : FrameHeader , image : ImageHeader ) : Planes {
317336 const ch = lf . modular . channels . slice ( lf . modular . nbMeta ) ;
318337 const out : Float32Array [ ] = [ ] ;
319- const toFloat = ( c : Channel , depth = image . bitDepth ) : Float32Array => {
320- const o = new Float32Array ( c . w * c . h ) ;
321- if ( depth . float ) for ( let i = 0 ; i < o . length ; i ++ ) o [ i ] = floatSample ( c . data [ i ] , depth . bits , depth . expBits ) ;
322- else {
323- const scale = 1 / ( 2 ** depth . bits - 1 ) ;
324- for ( let i = 0 ; i < o . length ; i ++ ) o [ i ] = c . data [ i ] * scale ;
325- }
326- return o ;
327- } ;
338+ const toFloat = ( c : Channel , depth = image . bitDepth ) : Float32Array => samplesToFloat ( c , depth ) ;
328339 if ( ! f . modular ) {
329340 // (the extra channels only)
330341 image . extraChannels . forEach ( ( ec , i ) => out . push ( toFloat ( ch [ i ] , ec . bitDepth ) ) ) ;
@@ -347,6 +358,74 @@ function modularToPlanes(lf: LfGlobal, f: FrameHeader, image: ImageHeader): Plan
347358 return { width : f . sampleWidth , height : f . sampleHeight , channels : out } ;
348359}
349360
361+ /**
362+ * The color channels of a lossless frame's Modular image (integer samples, not XYB, 1 or 3 channels) as interleaved
363+ * RGBA bytes like toRGBA makes of modularToPlanes' (the alpha of 255; the extra channels' planes as there), in threads
364+ * if there are; the transforms undone, an RCT of the colors applied first with the conversion
365+ */
366+ async function modularToBytes ( lf : LfGlobal , f : FrameHeader , image : ImageHeader , pool : FrameThreads ) : Promise < Planes > {
367+ const m = lf . modular , numColor = f . encodedColorChannels , n = f . width * f . height , first = m . transforms [ 0 ] ;
368+ const rct = numColor === 3 && first ?. id === TR_RCT && first . beginC === m . nbMeta ? first : null ;
369+ m . undo ( rct ? 1 : 0 ) ;
370+ const color = rct ? m . rctChannels ( rct ) : [ 0 , 1 , 2 ] . map ( c => m . channels [ m . nbMeta + ( numColor === 1 ? 0 : c ) ] . data ) ;
371+ const task = ( threaded : boolean ) : PostTask => ( {
372+ task : 'jxl-post' , kind : 'samples' , w : f . width , h : f . height , inputs : [ ] , outputs : [ ] , samples : threaded ? color . map ( sharedOf ) : color ,
373+ rctType : rct ? rct . rctType : - 1 , bits : image . bitDepth . bits , rgba : new Uint8Array ( threaded ? new SharedArrayBuffer ( 4 * n ) : new ArrayBuffer ( 4 * n ) ) ,
374+ } ) ;
375+ const rgba = await threaded ( pool , n , async threads => {
376+ const t = task ( true ) ;
377+ await runChunks ( threads , t , Math . ceil ( f . height / BAND ) , postChunks ( t ) ) ;
378+ // (the samples undone in shared memory; the pixels not: APIs like crypto.subtle and TextDecoder reject views of it)
379+ if ( rct ) for ( let c = 0 ; c < 3 ; c ++ ) m . channels [ m . nbMeta + c ] . data = ( t . samples as Int32Array [ ] ) [ c ] ;
380+ return new Uint8Array ( t . rgba as Uint8Array ) ;
381+ } , ( ) => {
382+ const t = task ( false ) ;
383+ samplesToBytes ( t . samples as Int32Array [ ] , t . rctType as number , t . bits as number , t . rgba as Uint8Array , 0 , n ) ;
384+ return t . rgba as Uint8Array ;
385+ } ) ;
386+ if ( rct ) {
387+ m . permuteRCT ( rct ) ;
388+ m . transforms . length = 0 ;
389+ }
390+ const ch = m . channels . slice ( m . nbMeta ) ;
391+ return { width : f . width , height : f . height , channels : image . extraChannels . map ( ( ec , i ) => samplesToFloat ( ch [ numColor + i ] , ec . bitDepth ) ) , rgba } ;
392+ }
393+
394+ /**
395+ * Samples i0 to i1 of 3 color channels as RGBA bytes (see modularToBytes): first undoing an RCT (of rctType, -1 for
396+ * none), the samples bits deep
397+ */
398+ function samplesToBytes ( samples : Int32Array [ ] , rctType : number , bits : number , rgba : Uint8Array , i0 : number , i1 : number ) : void {
399+ if ( rctType >= 0 ) rctSamples ( samples , rctType , i0 , i1 ) ;
400+ // (the RCT's channels go to their places)
401+ const order = rctType >= 0 ? rctOrder ( rctType ) : [ 0 , 1 , 2 ] , [ r , g , b ] = [ 0 , 1 , 2 ] . map ( c => samples [ order . indexOf ( c ) ] ) ;
402+ const max = 2 ** bits - 1 , scale = 1 / max ;
403+ // (the bytes of the samples 0 to max: through the same Float32 value as the planes)
404+ const table = bits <= 16 ? byteTable ( bits ) : null ;
405+ if ( table && littleEndian ) {
406+ // (a pixel per 32-bit word)
407+ const words = new Uint32Array ( rgba . buffer , rgba . byteOffset , rgba . length >> 2 ) ;
408+ const at = ( v : number ) : number => v >>> 0 <= max ? table [ v ] : v < 0 ? 0 : 255 ;
409+ for ( let i = i0 ; i < i1 ; i ++ ) words [ i ] = at ( r [ i ] ) | at ( g [ i ] ) << 8 | at ( b [ i ] ) << 16 | 0xff000000 ;
410+ } else {
411+ [ r , g , b ] . forEach ( ( data , c ) => {
412+ for ( let i = i0 , o = 4 * i0 + c ; i < i1 ; i ++ , o += 4 ) rgba [ o ] = table ? ( data [ i ] >>> 0 <= max ? table [ data [ i ] ] : data [ i ] < 0 ? 0 : 255 ) : byte ( Math . fround ( data [ i ] * scale ) ) ;
413+ } ) ;
414+ for ( let o = 4 * i0 + 3 ; o < 4 * i1 ; o += 4 ) rgba [ o ] = 255 ;
415+ }
416+ }
417+
418+ const byteTables = new Map < number , Uint8Array > ( ) ;
419+ // the bytes of the samples 0 to 2^bits - 1 (see samplesToBytes)
420+ function byteTable ( bits : number ) : Uint8Array {
421+ let table = byteTables . get ( bits ) ;
422+ if ( ! table ) {
423+ const scale = 1 / ( 2 ** bits - 1 ) ;
424+ byteTables . set ( bits , table = Uint8Array . from ( { length : 2 ** bits } , ( _ , v ) => byte ( Math . fround ( v * scale ) ) ) ) ;
425+ }
426+ return table ;
427+ }
428+
350429/** The restoration filters (sigma: per 8x8 block for VarDCT, blocks sigmaStride per row) */
351430function filter ( p : Planes , f : FrameHeader , image : ImageHeader , sigma : Float32Array | null , sigmaStride = 0 ) : Planes {
352431 if ( ! f . gab && ! f . epf ) return p ;
@@ -371,13 +450,16 @@ async function threaded<T>(pool: FrameThreads, pixels: number, inThreads: (threa
371450 return here ( ) ;
372451}
373452
374- // a typed array in shared memory (itself if it is)
453+ // planes in shared memory
375454const sharedPlanes = ( n : number , w : number , h : number ) : Float32Array [ ] => Array . from ( { length : n } , ( ) => new Float32Array ( new SharedArrayBuffer ( 4 * w * h ) ) ) ;
376455
377- /** The rows of the steps after decoding for threads (see postChunks): Gaborish, an EPF pass, XYB to bytes */
456+ /**
457+ * The rows of the steps after decoding for threads (see postChunks): Gaborish, an EPF pass, XYB to bytes, integer
458+ * samples to bytes
459+ */
378460export interface PostTask extends ChunkTask {
379461 task : 'jxl-post' ;
380- kind : 'gaborish' | 'epf' | 'xyb' ;
462+ kind : 'gaborish' | 'epf' | 'xyb' | 'samples' ;
381463 w : number ;
382464 h : number ;
383465 inputs : Float32Array [ ] ;
@@ -391,6 +473,9 @@ export interface PostTask extends ChunkTask {
391473 m ?: number [ ] ;
392474 bias ?: number [ ] ;
393475 k ?: number ;
476+ samples ?: Int32Array [ ] ;
477+ rctType ?: number ;
478+ bits ?: number ;
394479}
395480
396481// (the rows of a chunk of Gaborish and the XYB conversion; EPF's are its strips)
@@ -404,6 +489,7 @@ export function postChunks(t: PostTask): (chunk: number) => void {
404489 const scratch = epfScratch ( w ) ;
405490 return chunk => epfStrip ( inputs , outputs , w , h , t . epf as EPF , t . sigma ?? null , t . bw as number , t . step as number , chunk , scratch ) ;
406491 }
492+ if ( t . kind === 'samples' ) return chunk => samplesToBytes ( t . samples as Int32Array [ ] , t . rctType as number , t . bits as number , t . rgba as Uint8Array , chunk * BAND * w , Math . min ( h , ( chunk + 1 ) * BAND ) * w ) ;
407493 const [ x , y , b ] = inputs ;
408494 return chunk => xybToBytes ( x , y , b , t . m as number [ ] , t . bias as number [ ] , t . k as number , t . rgba as Uint8Array , chunk * BAND * w , Math . min ( h , ( chunk + 1 ) * BAND ) * w ) ;
409495}
@@ -431,6 +517,7 @@ async function filterThreaded(p: Planes, f: FrameHeader, image: ImageHeader, sig
431517
432518/** Upsamples the color channels and the extra channels to the frame's size */
433519function upsamplePlanes ( p : Planes , f : FrameHeader , image : ImageHeader ) : Planes {
520+ if ( f . upsampling === 1 && f . ecUpsampling . every ( u => u === 1 ) ) return p ;
434521 const numColor = p . channels . length - image . extraChannels . length , W = f . upsampledWidth , H = f . upsampledHeight ;
435522 const channels = p . channels . map ( ( c , i ) => {
436523 const up = i < numColor ? f . upsampling : f . ecUpsampling [ i - numColor ] ;
@@ -459,22 +546,29 @@ function floatSample(v: number, bits: number, expBits: number): number {
459546// ---- rendering: filters, features, upsampling, the color transform, blending, references
460547
461548// what follows the features: storing LF frames and references, the color transform, blending
549+ // whether a frame can be saved as a reference
550+ const referenced = ( f : FrameHeader ) : boolean => ! f . isLast && ( f . duration === 0 || f . saveAsReference !== 0 ) && f . type !== FRAME_LF ;
551+
552+ // whether only toRGBA uses the planes of a frame (if of the image's size): shown as it is, replacing all, not
553+ // referenced; with straight alpha only
554+ const shownAsIs = ( f : FrameHeader , image : ImageHeader ) : boolean => ( f . type === FRAME_REGULAR || f . type === FRAME_SKIP_PROGRESSIVE ) && ! f . haveCrop
555+ && f . blending . mode === BLEND_REPLACE && f . ecBlending . every ( b => b . mode === BLEND_REPLACE ) && f . width === image . width && f . height === image . height
556+ && ! referenced ( f ) && image . extraChannels . every ( e => e . type === EXTRA_ALPHA && ! e . alphaAssociated ) ;
557+
462558async function render ( p : Planes , f : FrameHeader , state : DecoderState , pool : FrameThreads ) : Promise < Frame > {
463559 const image = state . image ;
464560 let planes = p ;
465561 if ( f . type === FRAME_LF ) {
466562 state . lfFrames [ f . lfLevel - 1 ] = planes ;
467563 return { header : f , canvas : null } ;
468564 }
469- const canReference = ! f . isLast && ( f . duration === 0 || f . saveAsReference !== 0 ) && f . type !== FRAME_LF ;
565+ const canReference = referenced ( f ) ;
470566 if ( canReference && f . saveBeforeCT ) {
471567 state . references [ f . saveAsReference ] = planes ;
472568 state . referenceIsXYB [ f . saveAsReference ] = image . xybEncoded ;
473569 }
474570 const normal = f . type === FRAME_REGULAR || f . type === FRAME_SKIP_PROGRESSIVE ;
475- // (only toRGBA uses the planes of a frame shown as it is: replacing all, not referenced; with straight alpha only)
476- const replaces = ! f . haveCrop && f . blending . mode === BLEND_REPLACE && f . ecBlending . every ( b => b . mode === BLEND_REPLACE ) && p . width === image . width && p . height === image . height ;
477- const final = normal && replaces && ! canReference && image . extraChannels . every ( e => e . type === EXTRA_ALPHA && ! e . alphaAssociated ) ;
571+ const final = shownAsIs ( f , image ) && p . width === image . width && p . height === image . height ;
478572 // (XYB to bytes in threads)
479573 const bytes = state . output . bytesParams ( f . doYCbCr , final ) , input = planes ;
480574 planes = bytes ? await threaded ( pool , planes . width * planes . height , async threads => {
0 commit comments