@atesgoral/acb
Version:
Adobe Photoshop Color Book (ACB) encoder and decoder
278 lines (267 loc) • 8.69 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
var stream = require('stream');
var Parser = require('stream-parser');
var Ajv = require('ajv/dist/jtd');
function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
var Parser__default = /*#__PURE__*/_interopDefaultLegacy(Parser);
var Ajv__default = /*#__PURE__*/_interopDefaultLegacy(Ajv);
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 stream.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__default['default'](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__default['default']();
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');
}
exports.AcbStreamDecoder = AcbStreamDecoder;
exports.encodeAcb = encodeAcb;