1+ function readBits ( view , bitOffset , count ) {
2+ let value = 0 ;
3+ let done = 0 ;
4+
5+ while ( done < count ) {
6+ const bitInByte = ( bitOffset + done ) & 7 ;
7+ const take = Math . min ( 8 - bitInByte , count - done ) ;
8+ const byte = view . getUint8 ( ( bitOffset + done ) >> 3 ) ;
9+ const chunk = ( byte >> ( 8 - bitInByte - take ) ) & ( ( 1 << take ) - 1 ) ;
10+
11+ value = ( value * ( 1 << take ) ) + chunk ;
12+ done += take ;
13+ }
14+ return value ;
15+ }
16+
17+ function writeBits ( bytes , bitOffset , count , value ) {
18+ let done = 0 ;
19+
20+ while ( done < count ) {
21+ const bitInByte = ( bitOffset + done ) & 7 ;
22+ const take = Math . min ( 8 - bitInByte , count - done ) ;
23+ const idx = ( bitOffset + done ) >> 3 ;
24+ const shift = 8 - bitInByte - take ;
25+ const left = count - done - take ;
26+ const chunk = Math . floor ( value / ( 2 ** left ) ) & ( ( 1 << take ) - 1 ) ;
27+ const mask = ( ( 1 << take ) - 1 ) << shift ;
28+
29+ bytes [ idx ] = ( bytes [ idx ] & ~ mask ) | ( chunk << shift ) ;
30+ done += take ;
31+ }
32+ }
33+
34+ const BNA_TYPES = {
35+ bw : {
36+ bits : 8 ,
37+ name : "8-bit BW" ,
38+ toRGB ( pixel ) { return [ pixel , pixel , pixel ] } ,
39+ fromRGB : ( r , g , b ) => ( Math . max ( r , g , b ) )
40+ } ,
41+ b1 : {
42+ bits : 8 ,
43+ name : "8-bit RGB332" ,
44+ toRGB ( pixel ) {
45+ let r = pixel & 0xe0 ;
46+ r |= ( r >> 3 ) ;
47+ r |= ( r >> 3 ) ;
48+ let g = pixel & 0x1c ;
49+ g |= ( g << 3 ) | ( g >> 3 ) ;
50+ let b = pixel & 0x03 ;
51+ b |= b << 2 ;
52+ b |= b << 4 ;
53+ return [ r , g , b ] ;
54+ } ,
55+ fromRGB : ( r , g , b ) => ( r & 0xE0 ) | ( ( g & 0xE0 ) >> 3 ) | ( ( b & 0xC0 ) >> 6 )
56+ } ,
57+ b2 : {
58+ bits : 16 ,
59+ name : "16-bit RGB565" ,
60+ toRGB ( pixel ) {
61+ let r = ( pixel & 0xF800 ) >> 8 ;
62+ r |= r >> 5 ;
63+ let g = ( pixel & 0x07E0 ) >> 3 ;
64+ g |= g >> 6 ;
65+ let b = ( pixel & 0x001F ) << 3 ;
66+ b |= b >> 5 ;
67+ return [ r , g , b ] ;
68+ } ,
69+ fromRGB : ( r , g , b ) => ( ( r & 0xf8 ) << 8 ) | ( ( g & 0xFC ) << 3 ) | ( ( b & 0xF8 ) >> 3 )
70+ } ,
71+ b3 : {
72+ bits : 24 ,
73+ name : "24-bit RGB888" ,
74+ toRGB ( pixel ) { return [ ( pixel >> 16 ) & 0xff , ( pixel >> 8 ) & 0xff , pixel & 0xff ] } ,
75+ fromRGB : ( r , g , b ) => ( r << 16 ) | ( g << 8 ) | b
76+ }
77+ } ;
78+
79+ function frameBytes ( width , height , bits ) {
80+ return Math . ceil ( ( width * height * bits ) / 8 ) ;
81+ }
82+
183class BnaPlayer {
284 constructor ( canvas ) {
385 this . canvas = canvas ;
@@ -9,21 +91,33 @@ class BnaPlayer {
991 }
1092
1193 async load ( fileOrBuffer ) {
94+ this . stop ( ) ;
95+ this . meta = null ;
96+
1297 const buffer = fileOrBuffer instanceof ArrayBuffer
1398 ? fileOrBuffer
1499 : await fileOrBuffer . arrayBuffer ( ) ;
100+
101+ if ( buffer . byteLength < 8 ) throw new Error ( "File is too small to hold a header" ) ;
102+
15103 const view = new DataView ( buffer ) ;
16104 const signature = String . fromCharCode ( view . getUint8 ( 0 ) , view . getUint8 ( 1 ) ) ;
17105 const type = String . fromCharCode ( view . getUint8 ( 2 ) , view . getUint8 ( 3 ) ) ;
18106
19107 if ( signature !== 'bA' ) throw new Error ( "Invalid file signature" ) ;
20108
21- const bpp = type === 'b3' ? 3 : type === 'b2' ? 2 : type === 'b1' ? 1 : - 1 ;
109+ const codec = BNA_TYPES [ type ] ;
110+ if ( ! codec ) throw new Error ( `Unsupported type "${ type } "` ) ;
111+
22112 const w = view . getUint16 ( 4 , true ) ;
23113 const h = view . getUint16 ( 6 , true ) ;
24- const frames = Math . floor ( ( buffer . byteLength - 8 ) / ( w * h * bpp ) ) ;
114+ if ( w === 0 || h === 0 ) throw new Error ( "Width and height must be non-zero" ) ;
25115
26- this . meta = { buffer, view, w, h, bpp, frames, headerSize : 8 } ;
116+ const frameSize = frameBytes ( w , h , codec . bits ) ;
117+ const frames = Math . floor ( ( buffer . byteLength - 8 ) / frameSize ) ;
118+ if ( frames < 1 ) throw new Error ( `File holds no complete ${ w } x${ h } frame` ) ;
119+
120+ this . meta = { buffer, view, w, h, type, codec, frameSize, frames, headerSize : 8 } ;
27121 this . canvas . width = w ;
28122 this . canvas . height = h ;
29123 this . frameIdx = 0 ;
@@ -33,37 +127,15 @@ class BnaPlayer {
33127
34128 renderFrame ( idx ) {
35129 if ( ! this . meta ) return ;
36- const { headerSize, w, h, bpp , view, frames } = this . meta ;
130+ const { headerSize, w, h, codec , view, frames, frameSize } = this . meta ;
37131 const imageData = this . ctx . createImageData ( w , h ) ;
38132 const pixelsPerFrame = w * h ;
39- const frameOffset = headerSize + ( ( idx % frames ) * pixelsPerFrame * bpp ) ;
133+ const frameBit = ( headerSize + ( ( idx % frames ) * frameSize ) ) * 8 ;
40134
41135 for ( let i = 0 ; i < pixelsPerFrame ; i ++ ) {
42- let r , g , b ;
136+ const [ r , g , b ] = codec . toRGB ( readBits ( view , frameBit + ( i * codec . bits ) , codec . bits ) ) ;
43137 const pxIdx = i * 4 ;
44138
45- if ( bpp === 1 ) { // RGB332
46- const byte = view . getUint8 ( frameOffset + i ) ;
47- r = byte & 0xe0 ; // mask out the 3 bits of red at the start of the byte
48- r |= ( r >> 3 ) ; // extend limited 0-224 range to 0-252
49- r |= ( r >> 3 ) ; // extend limited 0-252 range to 0-255
50- g = byte & 0x1c ; // mask out the 3 bits of green in the middle of the byte
51- g |= ( g << 3 ) | ( r >> 3 ) ; // extend limited 0-34 range to 0-255
52- b = byte & 0x03 ; // mask out the 2 bits of blue at the end of the byte
53- b |= b << 2 ; // extend 0-3 range to 0-15
54- b |= b << 4 ;
55- } else if ( bpp === 2 ) { // RGB565
56- const word = view . getUint8 ( frameOffset + ( i * 2 ) ) << 8
57- | view . getUint8 ( frameOffset + ( i * 2 ) + 1 ) ;
58- r = ( word & 0xF800 ) >> 8 ;
59- g = ( word & 0x07E0 ) >> 3 ;
60- b = ( word & 0x001F ) << 3 ;
61- } else { // RGB888
62- r = view . getUint8 ( frameOffset + ( i * 3 ) ) ;
63- g = view . getUint8 ( frameOffset + ( i * 3 ) + 1 ) ;
64- b = view . getUint8 ( frameOffset + ( i * 3 ) + 2 ) ;
65- }
66-
67139 imageData . data [ pxIdx ] = r ;
68140 imageData . data [ pxIdx + 1 ] = g ;
69141 imageData . data [ pxIdx + 2 ] = b ;
@@ -74,7 +146,11 @@ class BnaPlayer {
74146
75147 play ( fps = this . fps ) {
76148 this . stop ( ) ;
77- this . fps = fps ;
149+ if ( ! this . meta ) return ;
150+
151+ const rate = Number ( fps ) ;
152+ if ( rate > 0 ) this . fps = rate ;
153+
78154 this . timer = setInterval ( ( ) => {
79155 this . renderFrame ( this . frameIdx ) ;
80156 this . frameIdx = ( this . frameIdx + 1 ) % this . meta . frames ;
@@ -83,6 +159,7 @@ class BnaPlayer {
83159
84160 stop ( ) {
85161 if ( this . timer ) clearInterval ( this . timer ) ;
162+ this . timer = null ;
86163 }
87164}
88165
@@ -92,64 +169,58 @@ class BnaEncoder {
92169 * @param {File } file - The GIF file
93170 * @param {number } width - Target width
94171 * @param {number } height - Target height
95- * @param {string } type - type: 'b1' (RGB332), 'b2' (RGB565), 'b3 ' (RGB888 )
172+ * @param {string } type - type code from BNA_TYPES, for example 'b1 ' (RGB332 )
96173 */
97174 static async fromGif ( file , width , height , type ) {
98- const decoder = new ImageDecoder ( { data : file . stream ( ) , type : "image/gif" } ) ;
99- await decoder . completed ;
100- const totalFrames = decoder . tracks . selectedTrack . frameCount ;
175+ const codec = BNA_TYPES [ type ] ;
176+ if ( ! codec ) throw new Error ( `Unsupported type "${ type } "` ) ;
177+ if ( ! Number . isInteger ( width ) || ! Number . isInteger ( height )
178+ || width < 1 || height < 1 || width > 65535 || height > 65535 ) {
179+ throw new Error ( "Width and height must be 1-65535" ) ;
180+ }
181+ if ( typeof ImageDecoder === 'undefined' ) throw new Error ( "This browser has no ImageDecoder" ) ;
101182
102- const bpp = ( type === 'b1' ) ? 1 : ( type === 'b2' ) ? 2 : ( type === 'b3' ) ? 3 : - 1 ;
103- const bytesPerFrame = width * height * bpp ;
104- const totalSize = 8 + ( bytesPerFrame * totalFrames ) ;
105- const buffer = new ArrayBuffer ( totalSize ) ;
106- const view = new DataView ( buffer ) ;
107- const uint8 = new Uint8Array ( buffer ) ;
108-
109- // Header: "bA" + type + width + height
110- uint8 [ 0 ] = 0x62 ; // b
111- uint8 [ 1 ] = 0x41 ; // A
112- uint8 [ 2 ] = type . charCodeAt ( 0 ) ;
113- uint8 [ 3 ] = type . charCodeAt ( 1 ) ;
114-
115- view . setUint16 ( 4 , width , true ) ;
116- view . setUint16 ( 6 , height , true ) ;
117-
118- const canvas = document . createElement ( 'canvas' ) ;
119- const ctx = canvas . getContext ( '2d' , { willReadFrequently : true } ) ;
120- canvas . width = width ;
121- canvas . height = height ;
122-
123- for ( let i = 0 ; i < totalFrames ; i ++ ) {
124- const { image } = await decoder . decode ( { frameIndex : i } ) ;
125- ctx . clearRect ( 0 , 0 , width , height ) ;
126- ctx . drawImage ( image , 0 , 0 , width , height ) ;
127- const rgba = ctx . getImageData ( 0 , 0 , width , height ) . data ;
128- const frameOffset = 8 + ( i * bytesPerFrame ) ;
129-
130- for ( let j = 0 ; j < width * height ; j ++ ) {
131- const r = rgba [ j * 4 ] ;
132- const g = rgba [ j * 4 + 1 ] ;
133- const b = rgba [ j * 4 + 2 ] ;
134- const pixelPos = frameOffset + ( j * bpp ) ;
135-
136- if ( bpp === 1 ) {
137- // RGB332
138- uint8 [ pixelPos ] = ( r & 0xE0 ) | ( ( g & 0xE0 ) >> 3 ) | ( ( b & 0xC0 ) >> 6 ) ;
139- } else if ( bpp === 2 ) {
140- // RGB565
141- uint8 [ pixelPos ] = ( r & 0xf8 ) | ( ( g & 0xE0 ) >> 5 ) ;
142- uint8 [ pixelPos + 1 ] = ( ( g & 0x1C ) << 5 ) | ( ( b & 0xF8 ) >> 3 ) ;
143- } else {
144- // RGB888
145- uint8 [ pixelPos ] = r ;
146- uint8 [ pixelPos + 1 ] = g ;
147- uint8 [ pixelPos + 2 ] = b ;
183+ const decoder = new ImageDecoder ( { data : file . stream ( ) , type : "image/gif" } ) ;
184+ try {
185+ await decoder . completed ;
186+ const totalFrames = decoder . tracks . selectedTrack . frameCount ;
187+
188+ const frameSize = frameBytes ( width , height , codec . bits ) ;
189+ const buffer = new ArrayBuffer ( 8 + ( frameSize * totalFrames ) ) ;
190+ const view = new DataView ( buffer ) ;
191+ const uint8 = new Uint8Array ( buffer ) ;
192+
193+ // Header: "bA" + type + width + height
194+ uint8 [ 0 ] = 0x62 ; // b
195+ uint8 [ 1 ] = 0x41 ; // A
196+ uint8 [ 2 ] = type . charCodeAt ( 0 ) ;
197+ uint8 [ 3 ] = type . charCodeAt ( 1 ) ;
198+
199+ view . setUint16 ( 4 , width , true ) ;
200+ view . setUint16 ( 6 , height , true ) ;
201+
202+ const canvas = document . createElement ( 'canvas' ) ;
203+ const ctx = canvas . getContext ( '2d' , { willReadFrequently : true } ) ;
204+ canvas . width = width ;
205+ canvas . height = height ;
206+
207+ for ( let i = 0 ; i < totalFrames ; i ++ ) {
208+ const { image } = await decoder . decode ( { frameIndex : i } ) ;
209+ ctx . clearRect ( 0 , 0 , width , height ) ; // no alpha in the format, transparency goes black
210+ ctx . drawImage ( image , 0 , 0 , width , height ) ;
211+ image . close ( ) ;
212+ const rgba = ctx . getImageData ( 0 , 0 , width , height ) . data ;
213+ const frameBit = ( 8 + ( i * frameSize ) ) * 8 ;
214+
215+ for ( let j = 0 ; j < width * height ; j ++ ) {
216+ const pixel = codec . fromRGB ( rgba [ j * 4 ] , rgba [ j * 4 + 1 ] , rgba [ j * 4 + 2 ] ) ;
217+ writeBits ( uint8 , frameBit + ( j * codec . bits ) , codec . bits , pixel ) ;
148218 }
149219 }
150- image . close ( ) ;
151- }
152220
153- return new Blob ( [ buffer ] , { type : 'application/octet-stream' } ) ;
221+ return new Blob ( [ buffer ] , { type : 'application/octet-stream' } ) ;
222+ } finally {
223+ decoder . close ( ) ;
224+ }
154225 }
155- }
226+ }
0 commit comments