2023-08-12 03:40:30 +00:00
// @ts-check
import { GifNodeType } from "./constants.js" ;
import crel , { fragment } from "../common/crel.js" ;
import getOwn from "../common/getOwn.js" ;
import { areBytesEqual } from "../common/bytes.js" ;
2023-08-16 15:46:31 +00:00
import pluralize from "../common/pluralize.js" ;
2023-08-16 16:45:11 +00:00
import { parseLogicalScreenDescriptorPackedField } from "./parseGif.js" ;
2023-08-12 03:40:30 +00:00
/** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */
const textEncoder = new TextEncoder ( ) ;
const VERSION _87 = textEncoder . encode ( "87a" ) ;
const VERSION _89 = textEncoder . encode ( "89a" ) ;
/ * *
* @ param { ( string | Node ) [ ] } children
* /
const p = ( ... children ) => crel ( "p" , { } , ... children ) ;
2023-08-16 16:45:11 +00:00
/ * *
* @ param { ( string | Node ) [ ] } children
* /
const li = ( ... children ) => crel ( "li" , { } , ... children ) ;
2023-08-16 16:59:33 +00:00
/ * *
* @ param { number } byte
* @ returns { string }
* /
const formatByte = ( byte ) => byte . toString ( 16 ) . padStart ( 2 , "0" ) ;
2023-08-16 15:46:31 +00:00
/ * *
* @ param { Uint8Array } bytes
* @ returns { number }
* /
const readGifUint = ( bytes ) => {
if ( bytes . length !== 2 ) throw new Error ( "Expected 2 bytes" ) ;
return ( bytes [ 1 ] << 8 ) | bytes [ 0 ] ;
} ;
2023-08-12 03:40:30 +00:00
/ * *
* @ typedef { object } NodeUi
* @ prop { string } title
* @ prop { HTMLElement | DocumentFragment } description
* /
/** @type {Record<string, (node: GifNode) => NodeUi>} */
const NODE _UI _FNS = {
[ GifNodeType . root ] : ( ) => ( {
title : "GIF file" ,
description : fragment ( ) ,
} ) ,
// Header
[ GifNodeType . header ] : ( ) => ( {
title : "GIF header" ,
description : fragment (
p ( 'GIFs start with a 6-byte header. This is typically the string "GIF87a" or "GIF89a", encoded as ASCII.' ) ,
) ,
} ) ,
[ GifNodeType . headerSignature ] : ( ) => ( {
title : "GIF signature" ,
description : p ( 'GIFs start with the string "GIF", encoded as ASCII.' ) ,
} ) ,
[ GifNodeType . headerVersion ] : ( { bytes } ) => {
/** @type {string} */ let end ;
if ( areBytesEqual ( bytes , VERSION _87 ) ) {
end = "87a" ;
} else if ( areBytesEqual ( bytes , VERSION _89 ) ) {
end = "89a" ;
} else {
end = "something unexpected! This might not be a valid GIF file" ;
}
return {
title : "GIF version" ,
description : p (
` The version of the GIF format. This is typically "87a" or "89a". In this case, the version is ${ end } . ` ,
) ,
} ;
} ,
2023-08-16 15:46:31 +00:00
// Logical screen descriptor
[ GifNodeType . logicalScreenDescriptor ] : ( ) => ( {
2023-08-16 16:59:33 +00:00
title : "Logical Screen Descriptor block" ,
2023-08-16 15:46:31 +00:00
description : p (
2023-08-16 16:59:33 +00:00
"The Logical Screen Descriptor contains information about the overall GIF, such as its dimensions." ,
2023-08-16 15:46:31 +00:00
) ,
} ) ,
[ GifNodeType . logicalScreenWidth ] : ( { bytes } ) => ( {
title : "Logical Screen Width" ,
description : p (
` The width of the image stored as a 16-bit little endian unsigned integer. In this case, the width is ${
pluralize ( readGifUint ( bytes ) , "byte" )
} . ` ,
) ,
} ) ,
[ GifNodeType . logicalScreenHeight ] : ( { bytes } ) => ( {
title : "Logical Screen Height" ,
description : p (
` The height of the image stored as a 16-bit little endian unsigned integer. In this case, the height is ${
pluralize ( readGifUint ( bytes ) , "byte" )
} . ` ,
) ,
} ) ,
2023-08-16 16:45:11 +00:00
[ GifNodeType . logicalScreenDescriptorPackedFields ] : ( { bytes } ) => {
const byte = bytes [ 0 ] ;
const packedField = parseLogicalScreenDescriptorPackedField ( byte ) ;
return {
title : "Logical Screen Descriptor packed fields" ,
description : fragment (
p ( "This byte contains several flags that control how the GIF is displayed. In binary, it looks like this:" ) ,
p ( bytes [ 0 ] . toString ( 2 ) . padStart ( 8 , "0" ) ) ,
crel (
"ul" ,
{ } ,
li (
packedField . globalColorTableFlag
? "The first bit is 1, which means that a global color table follows the logical screen descriptor. See below for more information about this table."
: "The first bit is 0, which means that this GIF has no global color table. This means that each frame has its own local color table." ,
) ,
li (
` The next three bits ( ${
packedField . colorResolution . toString ( 2 ) . padStart ( 3 , "0" )
} ) encode the color resolution . The bigger this number , the more colors this GIF can represent . This value is decoded as binary and then incremented by one , so the color resolution is $ { packedField . colorResolution } . ` ,
) ,
li (
` The next bit is ${
packedField . sortFlag ? 1 : 0
} , which means that the colors in the global color table are$ {
packedField . sortFlag ? "" : "n't"
2023-08-16 17:11:07 +00:00
} sorted . This used to be important when memory was more scarce , but it ' s not really important anymore . ` ,
2023-08-16 16:45:11 +00:00
) ,
li (
` The last three bits ( ${
( byte & 0b111 ) . toString ( 2 ) . padStart ( 3 , "0" )
2023-08-16 16:59:33 +00:00
} ) encode the number of colors of the global color table . To decode this field , you add 1 and then raise that to the power of 2 , so the size of the global color table is $ {
pluralize ( packedField . sizeOfGlobalColorTable , "color" )
2023-08-16 16:45:11 +00:00
} . ` ,
) ,
) ,
) ,
} ;
} ,
2023-08-16 15:46:31 +00:00
[ GifNodeType . logicalScreenBackgroundColorIndex ] : ( ) => ( {
title : "Background Color Index" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . logicalScreenPixelAspectRatio ] : ( ) => ( {
title : "Pixel Aspect Ratio" ,
description : p ( "TODO" ) ,
} ) ,
2023-08-16 16:59:33 +00:00
// Global color table
[ GifNodeType . globalColorTable ] : ( { children } ) => ( {
title : "Global Color Table" ,
description : p (
` The Global Color Table is a list of colors. Each color is encoded as three bytes: one for red, one for green, and one for blue. The number of colors in the table is ${
pluralize ( children ? . length || 0 , "color" )
} . ` ,
) ,
} ) ,
[ GifNodeType . globalColorTableColor ] : ( { bytes } ) => ( {
title : "Global Color Table color" ,
description : p (
"A color. It looks like this: " ,
crel (
"span" ,
{
"style" :
` display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb( ${
bytes . join ( "," )
} ) ` ,
} ,
` # ${ formatByte ( bytes [ 0 ] ) } ${ formatByte ( bytes [ 1 ] ) } ${
formatByte ( bytes [ 2 ] )
} ` ,
) ,
) ,
} ) ,
2023-08-12 03:40:30 +00:00
} ;
/ * *
* @ param { GifNode } node
* @ returns { NodeUi }
* /
export default ( node ) => {
const uiFn = getOwn ( NODE _UI _FNS , node . type ) ;
2023-08-16 15:46:31 +00:00
if ( ! uiFn ) {
throw new Error ( ` Found a node ( ${ node . type } ) with no matching UI function ` ) ;
}
2023-08-12 03:40:30 +00:00
return uiFn ( node ) ;
} ;