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": [
|
||||
"archive/*.js",
|
||||
"codecs/*.js",
|
||||
"image/parsers/*.js",
|
||||
"file/*.js",
|
||||
"io/*.js"
|
||||
],
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
coverage
|
||||
image/.DS_Store
|
||||
node_modules
|
||||
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
|
||||
MIME type string
|
||||
* 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
|
||||
ByteStream). For example, reading or peeking at N bits at a time.
|
||||
|
||||
|
@ -107,9 +107,30 @@ const mimeType = findMimeType(someArrayBuffer);
|
|||
|
||||
### bitjs.image
|
||||
|
||||
This package includes code for dealing with binary images. It includes a module for converting WebP
|
||||
images into alternative raster graphics formats (PNG/JPG).
|
||||
This package includes code for dealing with binary images. It includes general event-based parsers
|
||||
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
|
||||
import { convertWebPtoPNG, convertWebPtoJPG } from './bitjs/image/webp-shim/webp-shim.js';
|
||||
// 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