UNPKG

@atesgoral/acb

Version:

Adobe Photoshop Color Book (ACB) encoder and decoder

268 lines (261 loc) 8.29 kB
import { Transform } from 'stream'; import Parser from 'stream-parser'; import Ajv from 'ajv/dist/jtd'; function roundUp(value) { return Math.round(Math.abs(value)) * Math.sign(value); } const convert = { fromComponent: (component) => roundUp((component * 255) / 100), toComponent: (value) => roundUp((value * 100) / 255), }; const conversion = { RGB: { fromComponents: (components) => components, toComponents: (values) => values, }, CMYK: { fromComponents: (components) => components.map((component) => 255 - convert.fromComponent(component)), toComponents: (values) => values.map((value) => convert.toComponent(255 - value)), }, Lab: { fromComponents: (components) => [ convert.fromComponent(components[0]), components[1] + 128, components[2] + 128, ], toComponents: (values) => [ convert.toComponent(values[0]), values[1] - 128, values[2] - 128, ], }, }; function fromAscii(value) { return Buffer.from(value, 'ascii'); } function fromUInt16BE(value) { const chunk = Buffer.allocUnsafe(2); chunk.writeUInt16BE(value); return chunk; } function fromString(value) { const chunk = Buffer.allocUnsafe(4 + value.length * 2); chunk.write(value, 4, 'utf16le'); chunk.swap16(); chunk.writeUInt32BE(value.length); return chunk; } function fromComponents(components, colorModel) { return Buffer.from(conversion[colorModel].fromComponents(components)); } function toComponents(chunk, colorModel) { return conversion[colorModel].toComponents(Array.from(chunk)); } const IdToColorModel = { 0: 'RGB', 2: 'CMYK', 7: 'Lab', }; class AcbStreamDecoder extends Transform { constructor(options) { super(options); this.book = { id: 0, title: '', colorNamePrefix: '', colorNamePostfix: '', description: '', pageSize: 0, pageKey: 0, colorModel: 'RGB', colors: [], }; this.colorCount = 0; this.readAscii(4, this.onSignature); } onSignature(signature) { if (signature !== '8BCB') { return this.emit('error', new Error(`Not an ACB file: ${signature}`)); } this.readUInt16BE(this.onVersion); } onVersion(version) { if (version !== 1) { return this.emit('error', new Error(`Invalid version: ${version}`)); } this.readUInt16BE(this.onId); } onId(id) { this.book.id = id; this.readString(this.onTitle); } onTitle(title) { this.book.title = title; this.readString(this.onColorNamePrefix); } onColorNamePrefix(colorNamePrefix) { this.book.colorNamePrefix = colorNamePrefix; this.readString(this.onColorNamePostfix); } onColorNamePostfix(colorNamePostfix) { this.book.colorNamePostfix = colorNamePostfix; this.readString(this.onDescription); } onDescription(description) { this.book.description = description; this.readUInt16BE(this.onColorCount); } onColorCount(colorCount) { this.colorCount = colorCount; this.readUInt16BE(this.onPageSize); } onPageSize(pageSize) { this.book.pageSize = pageSize; this.readUInt16BE(this.onPageMidPoint); } onPageMidPoint(pageKey) { this.book.pageKey = pageKey; this.readUInt16BE(this.onColorModelId); } onColorModelId(colorModelId) { const colorModel = IdToColorModel[colorModelId]; if (!colorModel) { return this.emit('error', new Error(`Unknown color model: ${colorModelId}`)); } this.book.colorModel = colorModel; this.book.colors = []; this.checkReadNextColor(); } checkReadNextColor() { if (this.book.colors.length < this.colorCount) { this.readColor((color) => { this.book.colors.push(color); this.checkReadNextColor(); }); } else { this.readAscii(8, this.onSpotId); } } onSpotId(spotId) { if ((this.book.colorModel === 'Lab') !== (spotId === 'spflspot')) { return this.emit('error', new Error(`Lab color book without spot identifier`)); } this.emit('book', this.book); } readColor(callback) { const color = { name: '', code: '', components: [], }; this.readString((name) => { color.name = name; this.readAscii(6, (code) => { color.code = code.trimEnd(); this.readComponents((components) => { color.components = components; callback(color); }); }); }); } readComponents(callback) { this._bytes(this.book.colorModel === 'CMYK' ? 4 : 3, (chunk) => callback(toComponents(chunk, this.book.colorModel))); } readAscii(count, callback) { this._bytes(count, (chunk) => callback.call(this, chunk.toString('ascii'))); } readUInt16BE(callback) { this._bytes(2, (chunk) => callback.call(this, chunk.readUInt16BE())); } readUInt32BE(callback) { this._bytes(4, (chunk) => callback.call(this, chunk.readUInt32BE())); } readString(callback) { this.readUInt32BE((length) => { if (length) { this._bytes(length * 2, (chunk) => { const le = Buffer.from(chunk).swap16(); callback.call(this, le.toString('utf16le')); }); } else { callback.call(this, ''); } }); } // Mixed-in by Parser _bytes(_n, _cb) { } } Parser(AcbStreamDecoder.prototype); const schema = { properties: { id: { type: 'uint16' }, title: { type: 'string' }, colorNamePrefix: { type: 'string' }, colorNamePostfix: { type: 'string' }, description: { type: 'string' }, pageSize: { type: 'uint16' }, pageKey: { type: 'uint16' }, colorModel: { enum: ['RGB', 'CMYK', 'Lab'] }, colors: { elements: { properties: { name: { type: 'string' }, code: { type: 'string' }, components: { elements: { type: 'int16' }, }, }, }, }, }, }; const ajv = new Ajv(); const validate = ajv.compile(schema); const ColorModelToId = { RGB: 0, CMYK: 2, Lab: 7, }; const ColorModelComponents = { RGB: 3, CMYK: 4, Lab: 3, }; function* encodeAcb(book) { const valid = validate(book); if (!valid) { throw new Error(`Validation failed: ${JSON.stringify(validate.errors, null, 2)}`); } yield fromAscii('8BCB'); yield fromUInt16BE(1); yield fromUInt16BE(book.id); yield fromString(book.title); yield fromString(book.colorNamePrefix); yield fromString(book.colorNamePostfix); yield fromString(book.description); yield fromUInt16BE(book.colors.length); yield fromUInt16BE(book.pageSize); yield fromUInt16BE(book.pageKey); const colorModelId = ColorModelToId[book.colorModel]; const expectedComponents = ColorModelComponents[book.colorModel]; if (isNaN(colorModelId)) { throw new Error(`Unknown color model: ${book.colorModel}`); } yield fromUInt16BE(colorModelId); for (let color of book.colors) { if (color.code.length < 1 || color.code.length > 6) { throw new Error(`Invalid color code length: ${color.code.length}`); } if (color.components.length !== expectedComponents) { throw new Error(`Invalid component count: ${color.components.length}`); } yield fromString(color.name); yield fromAscii(color.code.padEnd(6, ' ')); yield fromComponents(color.components, book.colorModel); } yield fromAscii(book.colorModel === 'Lab' ? 'spflspot' : 'spflproc'); } export { AcbStreamDecoder, encodeAcb };