From 775746959ed7ea9a3cda6fd6fb916c9622d60d9e Mon Sep 17 00:00:00 2001 From: Evan Hahn Date: Wed, 16 Aug 2023 11:45:11 -0500 Subject: [PATCH] GIF: UI for LSD packed field --- public/gif/getNodeUi.js | 54 ++++++++++++++++++++++++++++++++++++--- public/gif/parseGif.js | 30 +++++++++++++++++++--- test/gif/parseGif.test.ts | 15 +++++++++++ 3 files changed, 92 insertions(+), 7 deletions(-) create mode 100644 test/gif/parseGif.test.ts diff --git a/public/gif/getNodeUi.js b/public/gif/getNodeUi.js index efe5339..c344b25 100644 --- a/public/gif/getNodeUi.js +++ b/public/gif/getNodeUi.js @@ -5,6 +5,7 @@ import crel, { fragment } from "../common/crel.js"; import getOwn from "../common/getOwn.js"; import { areBytesEqual } from "../common/bytes.js"; import pluralize from "../common/pluralize.js"; +import { parseLogicalScreenDescriptorPackedField } from "./parseGif.js"; /** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */ const textEncoder = new TextEncoder(); @@ -17,6 +18,11 @@ const VERSION_89 = textEncoder.encode("89a"); */ const p = (...children) => crel("p", {}, ...children); +/** + * @param {(string | Node)[]} children + */ +const li = (...children) => crel("li", {}, ...children); + /** * @param {Uint8Array} bytes * @returns {number} @@ -92,10 +98,50 @@ const NODE_UI_FNS = { }.`, ), }), - [GifNodeType.logicalScreenDescriptorPackedFields]: () => ({ - title: "Logical Screen Descriptor packed fields", - description: p("TODO"), - }), + [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" + } sorted. Sorting the color table can improve compression.${ + packedField.globalColorTableFlag + ? "" + : " However, there is no global color table in this GIF, so this bit is ignored." + }`, + ), + li( + `The last three bits (${ + (byte & 0b111).toString(2).padStart(3, "0") + }) encode the size 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, "byte") + }. `, + ), + ), + ), + }; + }, [GifNodeType.logicalScreenBackgroundColorIndex]: () => ({ title: "Background Color Index", description: p("TODO"), diff --git a/public/gif/parseGif.js b/public/gif/parseGif.js index 6409e5c..5ab7dda 100644 --- a/public/gif/parseGif.js +++ b/public/gif/parseGif.js @@ -3,11 +3,35 @@ import { GifNodeType } from "./constants.js"; /** @typedef {import("../../types/gif.d.ts").GifNode} GifNode */ +/** + * @typedef {object} LogicalScreenDescriptorPackedField + * @prop {boolean} globalColorTableFlag + * @prop {number} colorResolution + * @prop {boolean} sortFlag + * @prop {number} sizeOfGlobalColorTable + */ + +/** + * @param {number} byte + * @returns {LogicalScreenDescriptorPackedField} + */ +export const parseLogicalScreenDescriptorPackedField = (byte) => ({ + globalColorTableFlag: (byte & 0b10000000) !== 0, + colorResolution: ((byte & 0b01110000) >> 4) + 1, + sortFlag: (byte & 0b00001000) !== 0, + sizeOfGlobalColorTable: 2 ** ((byte & 0b00000111) + 1), +}); + /** * @param {Uint8Array} bytes * @returns {null | GifNode} The root node of the GIF tree, or null if the GIF is invalid. */ export default (bytes) => { + const widthBytes = bytes.subarray(6, 6 + 2); + const heightBytes = bytes.subarray(8, 8 + 2); + const packedFieldBytes = bytes.subarray(10, 10 + 1); + const packedFieldByte = packedFieldBytes[0]; + /** @type {GifNode[]} */ const children = [ { @@ -30,15 +54,15 @@ export default (bytes) => { children: [ { type: GifNodeType.logicalScreenWidth, - bytes: bytes.subarray(6, 6 + 2), + bytes: widthBytes, }, { type: GifNodeType.logicalScreenHeight, - bytes: bytes.subarray(8, 8 + 2), + bytes: heightBytes, }, { type: GifNodeType.logicalScreenDescriptorPackedFields, - bytes: bytes.subarray(10, 10 + 1), + bytes: packedFieldBytes, }, { type: GifNodeType.logicalScreenBackgroundColorIndex, diff --git a/test/gif/parseGif.test.ts b/test/gif/parseGif.test.ts new file mode 100644 index 0000000..b6bb6b7 --- /dev/null +++ b/test/gif/parseGif.test.ts @@ -0,0 +1,15 @@ +import { assertEquals } from "assert"; +import { parseLogicalScreenDescriptorPackedField } from "../../public/gif/parseGif.js"; + +Deno.test("parsing logical screen descriptor packed field", () => { + const byte = 0b10110011; + + const result = parseLogicalScreenDescriptorPackedField(byte); + + assertEquals(result, { + globalColorTableFlag: true, + colorResolution: 4, + sortFlag: false, + sizeOfGlobalColorTable: 16, + }); +});