mirror of
https://github.com/codedread/bitjs
synced 2025-10-03 09:39:16 +02:00
Add a GIF Parser into image/parsers
This commit is contained in:
parent
9558624b6a
commit
13d8a166bf
8 changed files with 574 additions and 3 deletions
1
.c8rc
1
.c8rc
|
@ -4,6 +4,7 @@
|
||||||
"include": [
|
"include": [
|
||||||
"archive/*.js",
|
"archive/*.js",
|
||||||
"codecs/*.js",
|
"codecs/*.js",
|
||||||
|
"image/parsers/*.js",
|
||||||
"file/*.js",
|
"file/*.js",
|
||||||
"io/*.js"
|
"io/*.js"
|
||||||
],
|
],
|
||||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
||||||
coverage
|
coverage
|
||||||
|
image/.DS_Store
|
||||||
node_modules
|
node_modules
|
||||||
tests/archive.spec.notready.js
|
tests/archive.spec.notready.js
|
||||||
|
|
27
README.md
27
README.md
|
@ -14,7 +14,7 @@ Includes:
|
||||||
* bitjs/codecs: Get the codec info of media containers in a ISO RFC6381
|
* bitjs/codecs: Get the codec info of media containers in a ISO RFC6381
|
||||||
MIME type string
|
MIME type string
|
||||||
* bitjs/file: Detect the type of file from its binary signature.
|
* bitjs/file: Detect the type of file from its binary signature.
|
||||||
* bitjs/image: Conversion of WebP images to PNG or JPEG.
|
* bitjs/image: Parsing GIF. Conversion of WebP to PNG or JPEG.
|
||||||
* bitjs/io: Low-level classes for interpreting binary data (BitStream
|
* bitjs/io: Low-level classes for interpreting binary data (BitStream
|
||||||
ByteStream). For example, reading or peeking at N bits at a time.
|
ByteStream). For example, reading or peeking at N bits at a time.
|
||||||
|
|
||||||
|
@ -107,9 +107,30 @@ const mimeType = findMimeType(someArrayBuffer);
|
||||||
|
|
||||||
### bitjs.image
|
### bitjs.image
|
||||||
|
|
||||||
This package includes code for dealing with binary images. It includes a module for converting WebP
|
This package includes code for dealing with binary images. It includes general event-based parsers
|
||||||
images into alternative raster graphics formats (PNG/JPG).
|
for images (GIF only, at the moment). It also includes a module for converting WebP images into
|
||||||
|
alternative raster graphics formats (PNG/JPG). This latter module is deprecated, now that WebP
|
||||||
|
images are well-supported in all browsers.
|
||||||
|
|
||||||
|
#### GIF Parser
|
||||||
|
```javascript
|
||||||
|
import { GifParser } from './bitjs/image/parsers/gif.js'
|
||||||
|
|
||||||
|
const parser = new GifParser(someArrayBuffer);
|
||||||
|
parser.addEventListener('application_extension', evt => {
|
||||||
|
const appId = evt.applicationExtension.applicationIdentifier
|
||||||
|
const appAuthCode = new TextDecoder().decode(
|
||||||
|
evt.applicationExtension.applicationAuthenticationCode);
|
||||||
|
if (appId === 'XMP Data' && appAuthCode === 'XMP') {
|
||||||
|
/** @type {Uint8Array} */
|
||||||
|
const appData = evt.applicationExtension.applicationData;
|
||||||
|
// Do something with appData (parse the XMP).
|
||||||
|
}
|
||||||
|
});
|
||||||
|
parser.start();
|
||||||
|
```
|
||||||
|
|
||||||
|
#### WebP Converter
|
||||||
```javascript
|
```javascript
|
||||||
import { convertWebPtoPNG, convertWebPtoJPG } from './bitjs/image/webp-shim/webp-shim.js';
|
import { convertWebPtoPNG, convertWebPtoJPG } from './bitjs/image/webp-shim/webp-shim.js';
|
||||||
// convertWebPtoPNG() takes in an ArrayBuffer containing the bytes of a WebP
|
// convertWebPtoPNG() takes in an ArrayBuffer containing the bytes of a WebP
|
||||||
|
|
6
image/parsers/README.md
Normal file
6
image/parsers/README.md
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
General-purpose, event-based parsers for digital images.
|
||||||
|
|
||||||
|
Currently only supports GIF.
|
||||||
|
|
||||||
|
Some nice implementations for HEIF, JPEG, PNG, TIFF here:
|
||||||
|
https://github.com/MikeKovarik/exifr/tree/master/src/file-parsers
|
486
image/parsers/gif.js
Normal file
486
image/parsers/gif.js
Normal file
|
@ -0,0 +1,486 @@
|
||||||
|
/*
|
||||||
|
* gif.js
|
||||||
|
*
|
||||||
|
* An event-based parser for GIF images.
|
||||||
|
*
|
||||||
|
* Licensed under the MIT License
|
||||||
|
*
|
||||||
|
* Copyright(c) 2023 Google Inc.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { BitStream } from '../../io/bitstream.js';
|
||||||
|
import { ByteStream } from '../../io/bytestream.js';
|
||||||
|
|
||||||
|
// https://www.w3.org/Graphics/GIF/spec-gif89a.txt
|
||||||
|
|
||||||
|
export const GifParseEventType = {
|
||||||
|
APPLICATION_EXTENSION: 'application_extension',
|
||||||
|
COMMENT_EXTENSION: 'comment_extension',
|
||||||
|
GRAPHIC_CONTROL_EXTENSION: 'graphic_control_extension',
|
||||||
|
HEADER: 'header',
|
||||||
|
LOGICAL_SCREEN: 'logical_screen',
|
||||||
|
PLAIN_TEXT_EXTENSION: 'plain_text_extension',
|
||||||
|
TABLE_BASED_IMAGE: 'table_based_image',
|
||||||
|
TRAILER: 'trailer',
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GifHeader
|
||||||
|
* @property {string} version
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GifHeaderParseEvent extends Event {
|
||||||
|
/** @param {GifHeader} header */
|
||||||
|
constructor(header) {
|
||||||
|
super(GifParseEventType.HEADER);
|
||||||
|
/** @type {GifHeader} */
|
||||||
|
this.header = header;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GifColor
|
||||||
|
* @property {number} red
|
||||||
|
* @property {number} green
|
||||||
|
* @property {number} blue
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GifLogicalScreen
|
||||||
|
* @property {number} logicalScreenWidth
|
||||||
|
* @property {number} logicalScreenHeight
|
||||||
|
* @property {boolean} globalColorTableFlag
|
||||||
|
* @property {number} colorResolution
|
||||||
|
* @property {boolean} sortFlag
|
||||||
|
* @property {number} globalColorTableSize
|
||||||
|
* @property {number} backgroundColorIndex
|
||||||
|
* @property {number} pixelAspectRatio
|
||||||
|
* @property {GifColor[]=} globalColorTable Only if globalColorTableFlag is true.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GifLogicalScreenParseEvent extends Event {
|
||||||
|
/** @param {GifLogicalScreen} */
|
||||||
|
constructor(logicalScreen) {
|
||||||
|
super(GifParseEventType.LOGICAL_SCREEN);
|
||||||
|
/** @type {GifLogicalScreen} */
|
||||||
|
this.logicalScreen = logicalScreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GifTableBasedImage
|
||||||
|
* @property {number} imageLeftPosition
|
||||||
|
* @property {number} imageTopPosition
|
||||||
|
* @property {number} imageWidth
|
||||||
|
* @property {number} imageHeight
|
||||||
|
* @property {boolean} localColorTableFlag
|
||||||
|
* @property {boolean} interlaceFlag
|
||||||
|
* @property {boolean} sortFlag
|
||||||
|
* @property {number} localColorTableSize
|
||||||
|
* @property {GifColor[]=} localColorTable Only if localColorTableFlag is true.
|
||||||
|
* @property {number} lzwMinimumCodeSize
|
||||||
|
* @property {Uint8Array} imageData
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GifTableBasedImageEvent extends Event {
|
||||||
|
/** @param {GifTableBasedImage} img */
|
||||||
|
constructor(img) {
|
||||||
|
super(GifParseEventType.TABLE_BASED_IMAGE);
|
||||||
|
/** @type {GifTableBasedImage} */
|
||||||
|
this.tableBasedImage = img;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GifGraphicControlExtension
|
||||||
|
* @property {number} disposalMethod
|
||||||
|
* @property {boolean} userInputFlag
|
||||||
|
* @property {boolean} transparentColorFlag
|
||||||
|
* @property {number} delayTime
|
||||||
|
* @property {number} transparentColorIndex
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GifGraphicControlExtensionEvent extends Event {
|
||||||
|
/** @param {GifGraphicControlExtension} ext */
|
||||||
|
constructor(ext) {
|
||||||
|
super(GifParseEventType.GRAPHIC_CONTROL_EXTENSION);
|
||||||
|
/** @type {GifGraphicControlExtension} */
|
||||||
|
this.graphicControlExtension = ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GifCommentExtension
|
||||||
|
* @property {string} comment
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GifCommentExtensionEvent extends Event {
|
||||||
|
/** @param {string} comment */
|
||||||
|
constructor(comment) {
|
||||||
|
super(GifParseEventType.COMMENT_EXTENSION);
|
||||||
|
/** @type {string} */
|
||||||
|
this.comment = comment;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GifPlainTextExtension
|
||||||
|
* @property {number} textGridLeftPosition
|
||||||
|
* @property {number} textGridTopPosition
|
||||||
|
* @property {number} textGridWidth
|
||||||
|
* @property {number} textGridHeight
|
||||||
|
* @property {number} characterCellWidth
|
||||||
|
* @property {number} characterCellHeight
|
||||||
|
* @property {number} textForegroundColorIndex
|
||||||
|
* @property {number} textBackgroundColorIndex
|
||||||
|
* @property {string} plainText
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GifPlainTextExtensionEvent extends Event {
|
||||||
|
/** @param {GifPlainTextExtension} ext */
|
||||||
|
constructor(ext) {
|
||||||
|
super(GifParseEventType.PLAIN_TEXT_EXTENSION);
|
||||||
|
/** @type {GifPlainTextExtension} */
|
||||||
|
this.plainTextExtension = ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef GifApplicationExtension
|
||||||
|
* @property {string} applicationIdentifier
|
||||||
|
* @property {Uint8Array} applicationAuthenticationCode
|
||||||
|
* @property {Uint8Array} applicationData
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GifApplicationExtensionEvent extends Event {
|
||||||
|
/** @param {GifApplicationExtension} ext */
|
||||||
|
constructor(ext) {
|
||||||
|
super(GifParseEventType.APPLICATION_EXTENSION);
|
||||||
|
/** @type {GifApplicationExtension} */
|
||||||
|
this.applicationExtension = ext;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GifTrailerEvent extends Event {
|
||||||
|
constructor() {
|
||||||
|
super(GifParseEventType.TRAILER);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The Grammar.
|
||||||
|
*
|
||||||
|
* <GIF Data Stream> ::= Header <Logical Screen> <Data>* Trailer
|
||||||
|
* <Logical Screen> ::= Logical Screen Descriptor [Global Color Table]
|
||||||
|
* <Data> ::= <Graphic Block> |
|
||||||
|
* <Special-Purpose Block>
|
||||||
|
* <Graphic Block> ::= [Graphic Control Extension] <Graphic-Rendering Block>
|
||||||
|
* <Graphic-Rendering Block> ::= <Table-Based Image> |
|
||||||
|
* Plain Text Extension
|
||||||
|
* <Table-Based Image> ::= Image Descriptor [Local Color Table] Image Data
|
||||||
|
* <Special-Purpose Block> ::= Application Extension |
|
||||||
|
* Comment Extension
|
||||||
|
*/
|
||||||
|
|
||||||
|
export class GifParser extends EventTarget {
|
||||||
|
/**
|
||||||
|
* @type {ByteStream}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
bstream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @type {string}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
version;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {ArrayBuffer} ab
|
||||||
|
*/
|
||||||
|
constructor(ab) {
|
||||||
|
super();
|
||||||
|
this.bstream = new ByteStream(ab);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Overridden so that the type hints for eventType are specific.
|
||||||
|
* @param {'application_extension'|'comment_extension'|'graphical_control_extension'|'header'|'logical_screen'|'plain_text_extension'|'table_based_image'|'trailer'} eventType
|
||||||
|
* @param {EventListenerOrEventListenerObject} listener
|
||||||
|
* @override
|
||||||
|
*/
|
||||||
|
addEventListener(eventType, listener) {
|
||||||
|
super.addEventListener(eventType, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {Promise<void>} A Promise that resolves when the parsing is complete.
|
||||||
|
*/
|
||||||
|
async start() {
|
||||||
|
// Header.
|
||||||
|
const gif = this.bstream.readString(3); // "GIF"
|
||||||
|
if (gif !== "GIF") throw `Not GIF: ${gif}`;
|
||||||
|
|
||||||
|
const version = this.version = this.bstream.readString(3); // "87a" or "89a"
|
||||||
|
if (!["87a", "89a"].includes(version)) throw `Bad version: ${version}`;
|
||||||
|
|
||||||
|
this.dispatchEvent(new GifHeaderParseEvent(
|
||||||
|
/** @type {GifHeader} */ ({ version })
|
||||||
|
));
|
||||||
|
|
||||||
|
// Logical Screen Descriptor.
|
||||||
|
const logicalScreenWidth = this.bstream.readNumber(2);
|
||||||
|
const logicalScreenHeight = this.bstream.readNumber(2);
|
||||||
|
|
||||||
|
const bitstream = new BitStream(this.bstream.readBytes(1).buffer, true);
|
||||||
|
const globalColorTableFlag = !!bitstream.readBits(1);
|
||||||
|
const colorResolution = bitstream.readBits(3) + 1;
|
||||||
|
const sortFlag = !!bitstream.readBits(1); // sortFlag
|
||||||
|
const globalColorTableSize = 2 ** (bitstream.readBits(3) + 1);
|
||||||
|
const backgroundColorIndex = this.bstream.readNumber(1);
|
||||||
|
const pixelAspectRatio = this.bstream.readNumber(1);
|
||||||
|
|
||||||
|
// Global Color Table
|
||||||
|
let globalColorTable = undefined;
|
||||||
|
if (globalColorTableFlag) {
|
||||||
|
globalColorTable = [];
|
||||||
|
// Series of R,G,B.
|
||||||
|
for (let c = 0; c < globalColorTableSize; ++c) {
|
||||||
|
globalColorTable.push(/** @type {GifColor} */ ({
|
||||||
|
red: this.bstream.readNumber(1),
|
||||||
|
green: this.bstream.readNumber(1),
|
||||||
|
blue: this.bstream.readNumber(1),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.dispatchEvent(new GifLogicalScreenParseEvent(
|
||||||
|
/** @type {GifLogicalScreen} */ ({
|
||||||
|
logicalScreenWidth,
|
||||||
|
logicalScreenHeight,
|
||||||
|
globalColorTableFlag,
|
||||||
|
colorResolution,
|
||||||
|
sortFlag,
|
||||||
|
globalColorTableSize,
|
||||||
|
backgroundColorIndex,
|
||||||
|
pixelAspectRatio,
|
||||||
|
globalColorTable,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
while (this.readGraphicBlock()) {
|
||||||
|
// Read a graphic block
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @returns {boolean} True if this was not the last block.
|
||||||
|
*/
|
||||||
|
readGraphicBlock() {
|
||||||
|
let nextByte = this.bstream.readNumber(1);
|
||||||
|
|
||||||
|
// Image Descriptor.
|
||||||
|
if (nextByte === 0x2C) {
|
||||||
|
const imageLeftPosition = this.bstream.readNumber(2);
|
||||||
|
const imageTopPosition = this.bstream.readNumber(2);
|
||||||
|
const imageWidth = this.bstream.readNumber(2);
|
||||||
|
const imageHeight = this.bstream.readNumber(2);
|
||||||
|
|
||||||
|
const bitstream = new BitStream(this.bstream.readBytes(1).buffer, true);
|
||||||
|
const localColorTableFlag = !!bitstream.readBits(1);
|
||||||
|
const interlaceFlag = !!bitstream.readBits(1);
|
||||||
|
const sortFlag = !!bitstream.readBits(1);
|
||||||
|
bitstream.readBits(2); // reserved
|
||||||
|
const localColorTableSize = 2 ** (bitstream.readBits(3) + 1);
|
||||||
|
|
||||||
|
let localColorTable = undefined;
|
||||||
|
if (localColorTableFlag) {
|
||||||
|
// this.bstream.readBytes(3 * localColorTableSize);
|
||||||
|
localColorTable = [];
|
||||||
|
// Series of R,G,B.
|
||||||
|
for (let c = 0; c < localColorTableSize; ++c) {
|
||||||
|
localColorTable.push(/** @type {GifColor} */ ({
|
||||||
|
red: this.bstream.readNumber(1),
|
||||||
|
green: this.bstream.readNumber(1),
|
||||||
|
blue: this.bstream.readNumber(1),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table-Based Image.
|
||||||
|
const lzwMinimumCodeSize = this.bstream.readNumber(1);
|
||||||
|
const bytesArr = [];
|
||||||
|
let bytes;
|
||||||
|
let totalNumBytes = 0;
|
||||||
|
while ((bytes = this.readSubBlock())) {
|
||||||
|
totalNumBytes += bytes.byteLength;
|
||||||
|
bytesArr.push(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const imageData = new Uint8Array(totalNumBytes);
|
||||||
|
let ptr = 0;
|
||||||
|
for (const arr of bytesArr) {
|
||||||
|
imageData.set(arr, ptr);
|
||||||
|
ptr += arr.byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new GifTableBasedImageEvent(
|
||||||
|
/** @type {GifTableBasedImage} */ ({
|
||||||
|
imageLeftPosition,
|
||||||
|
imageTopPosition,
|
||||||
|
imageWidth,
|
||||||
|
imageHeight,
|
||||||
|
localColorTableFlag,
|
||||||
|
interlaceFlag,
|
||||||
|
sortFlag,
|
||||||
|
localColorTableSize,
|
||||||
|
localColorTable,
|
||||||
|
lzwMinimumCodeSize,
|
||||||
|
imageData,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Extensions.
|
||||||
|
else if (nextByte === 0x21) {
|
||||||
|
if (this.version !== '89a') {
|
||||||
|
throw `Found Extension Introducer (0x21) but was not GIF 89a: ${this.version}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = this.bstream.readNumber(1);
|
||||||
|
|
||||||
|
// Graphic Control Extension.
|
||||||
|
if (label === 0xF9) {
|
||||||
|
const blockSize = this.bstream.readNumber(1);
|
||||||
|
if (blockSize !== 4) throw `GCE: Block size of ${blockSize}`;
|
||||||
|
|
||||||
|
// Packed Fields.
|
||||||
|
const bitstream = new BitStream(this.bstream.readBytes(1).buffer, true);
|
||||||
|
bitstream.readBits(3); // Reserved
|
||||||
|
const disposalMethod = bitstream.readBits(3);
|
||||||
|
const userInputFlag = !!bitstream.readBits(1);
|
||||||
|
const transparentColorFlag = !!bitstream.readBits(1);
|
||||||
|
|
||||||
|
const delayTime = this.bstream.readNumber(2);
|
||||||
|
const transparentColorIndex = this.bstream.readNumber(1);
|
||||||
|
const blockTerminator = this.bstream.readNumber(1);
|
||||||
|
if (blockTerminator !== 0) throw `GCE: Block terminator of ${blockTerminator}`;
|
||||||
|
|
||||||
|
this.dispatchEvent(new GifGraphicControlExtensionEvent(
|
||||||
|
/** @type {GifGraphicControlExtension} */ ({
|
||||||
|
disposalMethod,
|
||||||
|
userInputFlag,
|
||||||
|
transparentColorFlag,
|
||||||
|
delayTime,
|
||||||
|
transparentColorIndex,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Comment Extension.
|
||||||
|
else if (label === 0xFE) {
|
||||||
|
let bytes;
|
||||||
|
let comment = '';
|
||||||
|
while ((bytes = this.readSubBlock())) {
|
||||||
|
comment += new TextDecoder().decode(bytes);
|
||||||
|
}
|
||||||
|
this.dispatchEvent(new GifCommentExtensionEvent(comment));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plain Text Extension.
|
||||||
|
else if (label === 0x01) {
|
||||||
|
const blockSize = this.bstream.readNumber(1);
|
||||||
|
if (blockSize !== 12) throw `PTE: Block size of ${blockSize}`;
|
||||||
|
|
||||||
|
const textGridLeftPosition = this.bstream.readNumber(2);
|
||||||
|
const textGridTopPosition = this.bstream.readNumber(2);
|
||||||
|
const textGridWidth = this.bstream.readNumber(2);
|
||||||
|
const textGridHeight = this.bstream.readNumber(2);
|
||||||
|
const characterCellWidth = this.bstream.readNumber(1);
|
||||||
|
const characterCellHeight = this.bstream.readNumber(1);
|
||||||
|
const textForegroundColorIndex = this.bstream.readNumber(1);
|
||||||
|
const textBackgroundColorIndex = this.bstream.readNumber(1);
|
||||||
|
let bytes;
|
||||||
|
let plainText = ''
|
||||||
|
while ((bytes = this.readSubBlock())) {
|
||||||
|
plainText += new TextDecoder().decode(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new GifPlainTextExtensionEvent(
|
||||||
|
/** @type {GifPlainTextExtension} */ ({
|
||||||
|
textGridLeftPosition,
|
||||||
|
textGridTopPosition,
|
||||||
|
textGridWidth,
|
||||||
|
textGridHeight,
|
||||||
|
characterCellWidth,
|
||||||
|
characterCellHeight,
|
||||||
|
textForegroundColorIndex,
|
||||||
|
textBackgroundColorIndex,
|
||||||
|
plainText,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Application Extension.
|
||||||
|
else if (label === 0xFF) {
|
||||||
|
const blockSize = this.bstream.readNumber(1);
|
||||||
|
if (blockSize !== 11) throw `AE: Block size of ${blockSize}`;
|
||||||
|
|
||||||
|
// TODO: Extract EXIF / XMP / whatever.
|
||||||
|
const applicationIdentifier = this.bstream.readString(8);
|
||||||
|
const applicationAuthenticationCode = this.bstream.readBytes(3);
|
||||||
|
const bytesArr = [];
|
||||||
|
let bytes;
|
||||||
|
let totalNumBytes = 0;
|
||||||
|
while ((bytes = this.readSubBlock())) {
|
||||||
|
totalNumBytes += bytes.byteLength;
|
||||||
|
bytesArr.push(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
const applicationData = new Uint8Array(totalNumBytes);
|
||||||
|
let ptr = 0;
|
||||||
|
for (const arr of bytesArr) {
|
||||||
|
applicationData.set(arr, ptr);
|
||||||
|
ptr += arr.byteLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dispatchEvent(new GifApplicationExtensionEvent(
|
||||||
|
/** {@type GifApplicationExtension} */ ({
|
||||||
|
applicationIdentifier,
|
||||||
|
applicationAuthenticationCode,
|
||||||
|
applicationData,
|
||||||
|
})
|
||||||
|
));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
else {
|
||||||
|
throw `Unrecognized extension label=0x${label.toString(16)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (nextByte === 0x3B) {
|
||||||
|
this.dispatchEvent(new GifTrailerEvent());
|
||||||
|
// Read the trailer.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
throw `Unknown marker: 0x${nextByte.toString(16)}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @private
|
||||||
|
* @returns {Uint8Array} Data from the sub-block, or null if this was the last, zero-length block.
|
||||||
|
*/
|
||||||
|
readSubBlock() {
|
||||||
|
let subBlockSize = this.bstream.readNumber(1);
|
||||||
|
if (subBlockSize === 0) return null;
|
||||||
|
return this.bstream.readBytes(subBlockSize);
|
||||||
|
}
|
||||||
|
}
|
56
tests/image-parsers-gif.spec.js
Normal file
56
tests/image-parsers-gif.spec.js
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
import * as fs from 'node:fs';
|
||||||
|
import 'mocha';
|
||||||
|
import { expect } from 'chai';
|
||||||
|
import { GifParser } from '../image/parsers/gif.js';
|
||||||
|
|
||||||
|
const COMMENT_GIF = `tests/image-testfiles/comment.gif`;
|
||||||
|
const XMP_GIF = 'tests/image-testfiles/xmp.gif';
|
||||||
|
|
||||||
|
describe('bitjs.image.parsers.GifParser', () => {
|
||||||
|
it('parses GIF with Comment Extension', async () => {
|
||||||
|
const nodeBuf = fs.readFileSync(COMMENT_GIF);
|
||||||
|
const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length);
|
||||||
|
|
||||||
|
const parser = new GifParser(ab);
|
||||||
|
let trailerFound = false;
|
||||||
|
let comment;
|
||||||
|
parser.addEventListener('logical_screen', evt => {
|
||||||
|
const {logicalScreenWidth, logicalScreenHeight} = evt.logicalScreen;
|
||||||
|
expect(logicalScreenWidth).equals(32);
|
||||||
|
expect(logicalScreenHeight).equals(52);
|
||||||
|
});
|
||||||
|
parser.addEventListener('table_based_image', evt => {
|
||||||
|
const {imageWidth, imageHeight} = evt.tableBasedImage;
|
||||||
|
expect(imageWidth).equals(32);
|
||||||
|
expect(imageHeight).equals(52);
|
||||||
|
});
|
||||||
|
parser.addEventListener('comment_extension', evt => comment = evt.comment);
|
||||||
|
parser.addEventListener('trailer', evt => trailerFound = true);
|
||||||
|
|
||||||
|
await parser.start();
|
||||||
|
|
||||||
|
expect(trailerFound).equals(true);
|
||||||
|
expect(comment).equals('JEFF!');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('parses GIF with Application Extension', async () => {
|
||||||
|
const nodeBuf = fs.readFileSync(XMP_GIF);
|
||||||
|
const ab = nodeBuf.buffer.slice(nodeBuf.byteOffset, nodeBuf.byteOffset + nodeBuf.length);
|
||||||
|
|
||||||
|
const parser = new GifParser(ab);
|
||||||
|
let appId;
|
||||||
|
let appAuthCode;
|
||||||
|
let hasAppData = false;
|
||||||
|
parser.addEventListener('application_extension', evt => {
|
||||||
|
appId = evt.applicationExtension.applicationIdentifier
|
||||||
|
appAuthCode = new TextDecoder().decode(
|
||||||
|
evt.applicationExtension.applicationAuthenticationCode);
|
||||||
|
hasAppData = evt.applicationExtension.applicationData.byteLength > 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
await parser.start();
|
||||||
|
|
||||||
|
expect(appId).equals("XMP Data");
|
||||||
|
expect(appAuthCode).equals('XMP');
|
||||||
|
});
|
||||||
|
});
|
BIN
tests/image-testfiles/comment.gif
Normal file
BIN
tests/image-testfiles/comment.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 B |
BIN
tests/image-testfiles/xmp.gif
Normal file
BIN
tests/image-testfiles/xmp.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.2 KiB |
Loading…
Add table
Add a link
Reference in a new issue