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 (
2023-11-10 18:01:53 +00:00
p (
'GIFs start with a 6-byte header. This is typically the string "GIF87a" or "GIF89a", encoded as ASCII.' ,
) ,
2023-08-12 03:40:30 +00:00
) ,
} ) ,
[ 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 ${
2023-11-10 18:01:53 +00:00
pluralize (
readGifUint ( bytes ) ,
"byte" ,
)
2023-08-16 15:46:31 +00:00
} . ` ,
) ,
} ) ,
[ 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 ${
2023-11-10 18:01:53 +00:00
pluralize (
readGifUint ( bytes ) ,
"byte" ,
)
2023-08-16 15:46:31 +00:00
} . ` ,
) ,
} ) ,
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 (
2023-11-10 18:01:53 +00:00
p (
"This byte contains several flags that control how the GIF is displayed. In binary, it looks like this:" ,
) ,
2023-08-16 16:45:11 +00:00
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 ( ${
2023-11-10 18:01:53 +00:00
packedField . colorResolution
. toString ( 2 )
. padStart (
3 ,
"0" ,
)
2023-08-16 16:45:11 +00:00
} ) 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 ( ${
2023-11-10 18:01:53 +00:00
( 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 $ {
2023-11-10 18:01:53 +00:00
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 ${
2023-11-10 18:01:53 +00:00
pluralize (
children ? . length || 0 ,
"color" ,
)
2023-08-16 16:59:33 +00:00
} . ` ,
) ,
} ) ,
[ GifNodeType . globalColorTableColor ] : ( { bytes } ) => ( {
title : "Global Color Table color" ,
description : p (
"A color. It looks like this: " ,
crel (
"span" ,
{
2023-11-10 18:01:53 +00:00
style :
2023-08-16 16:59:33 +00:00
` display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb( ${
2023-11-10 18:01:53 +00:00
bytes . join (
"," ,
)
2023-08-16 16:59:33 +00:00
} ) ` ,
} ,
` # ${ formatByte ( bytes [ 0 ] ) } ${ formatByte ( bytes [ 1 ] ) } ${
formatByte ( bytes [ 2 ] )
} ` ,
) ,
) ,
} ) ,
2023-09-16 19:06:58 +00:00
// Image descriptor
[ GifNodeType . imageSection ] : ( ) => ( {
title : "Image" ,
2023-11-09 15:11:24 +00:00
description : p ( "A single frame of this GIF." ) ,
2023-09-16 19:06:58 +00:00
} ) ,
[ GifNodeType . imageDescriptor ] : ( ) => ( {
title : "Image Descriptor" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . imageSeparator ] : ( ) => ( {
title : "Image Separator" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . imageDescriptorLeftPosition ] : ( ) => ( {
title : "Image Left Position" ,
description : p ( "The X coordinate of the top left corner of this frame." ) ,
} ) ,
[ GifNodeType . imageDescriptorTopPosition ] : ( ) => ( {
title : "Image Top Position" ,
description : p ( "The Y coordinate of the top left corner of this frame." ) ,
} ) ,
[ GifNodeType . imageDescriptorWidth ] : ( ) => ( {
title : "Image Width" ,
description : p ( "The width of this frame." ) ,
} ) ,
[ GifNodeType . imageDescriptorHeight ] : ( ) => ( {
title : "Image Height" ,
description : p ( "The height of this frame." ) ,
} ) ,
[ GifNodeType . imageDescriptorPackedFields ] : ( ) => ( {
title : "Image Descriptor packed fields" ,
description : p ( "TODO" ) ,
} ) ,
// Local color table
[ GifNodeType . localColorTable ] : ( { children } ) => ( {
2023-11-09 15:30:29 +00:00
title : "Local Color Table" ,
2023-09-16 19:06:58 +00:00
description : p (
` The Local Color Table is a list of colors for this frame. 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 ${
2023-11-10 18:01:53 +00:00
pluralize (
children ? . length || 0 ,
"color" ,
)
2023-09-16 19:06:58 +00:00
} . ` ,
) ,
} ) ,
[ GifNodeType . localColorTableColor ] : ( { bytes } ) => ( {
// TODO: DRY this out with the global color above? Or maybe just use a single node type?
title : "Local Color Table color" ,
description : p (
"A color. It looks like this: " ,
crel (
"span" ,
{
2023-11-10 18:01:53 +00:00
style :
2023-09-16 19:06:58 +00:00
` display:inline-block;width:1em;height:1em;vertical-align:middle;color:transparent;background:rgb( ${
2023-11-10 18:01:53 +00:00
bytes . join (
"," ,
)
2023-09-16 19:06:58 +00:00
} ) ` ,
} ,
` # ${ formatByte ( bytes [ 0 ] ) } ${ formatByte ( bytes [ 1 ] ) } ${
formatByte ( bytes [ 2 ] )
} ` ,
) ,
) ,
} ) ,
// Image data
[ GifNodeType . imageData ] : ( ) => ( {
title : "Image Data" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . imageDataLzwMinimumCodeSize ] : ( ) => ( {
title : "LZW Minimum Code Size" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . imageDataSubBlock ] : ( ) => ( {
title : "Image Data Sub-Block" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . imageDataSubBlockSize ] : ( ) => ( {
title : "Image Data Sub-Block Size" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . imageDataSubBlockData ] : ( ) => ( {
title : "Image Data Sub-Block Data" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . imageDataTerminator ] : ( ) => ( {
title : "Image Data Terminator" ,
description : p ( "TODO" ) ,
} ) ,
// Generic extension stuff
[ GifNodeType . extensionBlockIntroducer ] : ( ) => ( {
title : "Extension Block Introducer" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . extensionBlockSize ] : ( ) => ( {
title : "Extension Block Size" ,
description : p ( "TODO" ) ,
} ) ,
2023-11-10 18:01:53 +00:00
[ GifNodeType . extensionBlockTerminator ] : ( ) => ( {
title : "Extension Block Terminator" ,
description : p ( "A zero byte indicating the end of this extension block." ) ,
} ) ,
// Application extension block
[ GifNodeType . applicationExtensionBlock ] : ( ) => ( {
title : "Application Extension Block" ,
description : p (
'A block for application-specific extensions. A very common extension is the "NETSCAPE2.0" extension which specifies the number of loops.' ,
) ,
} ) ,
[ GifNodeType . applicationIdentifier ] : ( { bytes } ) => ( {
title : "Application Identifier" ,
description : p (
"An 8-byte string identifying the application. In this case, the identifier is " ,
crel ( "code" , { } , new TextDecoder ( ) . decode ( bytes ) ) ,
) ,
} ) ,
[ GifNodeType . applicationAuthenticationCode ] : ( ) => ( {
title : "Application Authentication Code" ,
description : p ( "Three bytes identifying the application." ) ,
} ) ,
[ GifNodeType . applicationSubBlock ] : ( { bytes } ) => {
const isTerminator = areBytesEqual ( bytes , new Uint8Array ( [ 0 ] ) ) ;
return {
title : "Application Sub-Block" ,
description : p (
"A sub-block of data for this application." +
( isTerminator
? " This byte is empty, meaning it's the end of the sub-blocks and this application extension block."
: "" ) ,
) ,
} ;
} ,
[ GifNodeType . applicationSubBlockSize ] : ( { bytes } ) => ( {
title : "Application Sub-Block Size" ,
description : p ( ` The size of this application sub-block: ${ bytes [ 0 ] } . ` ) ,
} ) ,
[ GifNodeType . applicationSubBlockData ] : ( ) => ( {
title : "Application Sub-Block Data" ,
description : p ( "The data for this application sub-block." ) ,
} ) ,
// Comment extension block
[ GifNodeType . commentSubBlock ] : ( ) => ( {
title : "Comment Sub-Block" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . commentSubBlockSize ] : ( ) => ( {
title : "Comment Sub-Block Size" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . commentSubBlockData ] : ( ) => ( {
title : "Comment Sub-Block Data" ,
description : p ( "TODO" ) ,
} ) ,
// Graphic control extension block
[ GifNodeType . graphicControlExtensionBlock ] : ( ) => ( {
title : "Graphic Control Extension Block" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . graphicControlExtensionPackedFields ] : ( ) => ( {
title : "Graphic Control Extension packed fields" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . graphicControlExtensionDelayTime ] : ( ) => ( {
title : "Graphic Control Extension Delay Time" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . graphicControlExtensionTransparentColorIndex ] : ( ) => ( {
title : "Graphic Control Extension Transparent Color Index" ,
description : p ( "TODO" ) ,
} ) ,
// Plain text extension block
[ GifNodeType . plainTextExtensionBlock ] : ( ) => ( {
title : "Plain Text Extension Block" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextExtensionBlockTextGridLeftPosition ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextExtensionBlockTextGridTopPosition ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextExtensionBlockTextGridWidth ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextExtensionBlockTextGridHeight ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextExtensionBlockCharacterCellWidth ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextExtensionBlockCharacterCellHeight ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextExtensionBlockTextForegroundColorIndex ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextExtensionBlockTextBackgroundColorIndex ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextSubBlock ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextSubBlockSize ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . plainTextSubBlockData ] : ( ) => ( {
title : "TODO" ,
description : p ( "TODO" ) ,
} ) ,
2023-09-16 19:06:58 +00:00
// Unknown extension block
[ GifNodeType . unknownExtensionBlock ] : ( ) => ( {
title : "Unknown Extension Block" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . unknownExtensionBlockLabel ] : ( ) => ( {
title : "Unknown Extension Block Label" ,
description : p ( "TODO" ) ,
} ) ,
[ GifNodeType . unknownExtensionBlockData ] : ( ) => ( {
title : "Unknown Extension Block Data" ,
description : p ( "TODO" ) ,
} ) ,
2023-11-09 15:20:48 +00:00
// Terminator
[ GifNodeType . gifTerminator ] : ( ) => ( {
title : "GIF Terminator" ,
description : p (
"The terminator is a one-byte block indicating the end of our GIF. We're done!" ,
) ,
} ) ,
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 ) ;
} ;