UNPKG

@aeolun/muhammara

Version:

Create, read and modify PDF files and streams. A drop in replacement for hummusjs PDF library

397 lines (361 loc) 11.6 kB
const muhammara = require("../muhammara"); const fs = require("fs"); this.knownColors = { // knownColors.colorspace.colorName = value rgb: { red: "ff0000", green: "00ff00", blue: "0000ff", }, cmyk: { cyan: "ff000000", magenta: "00ff0000", yellow: "0000ff00", black: "000000ff", }, gray: { white: "ff", black: "00", }, separation: { // meant for printing, so using cmyk values initially cyan: "ff000000", magenta: "00ff0000", yellow: "0000ff00", black: "000000ff", nans: "%,35,6,0", // a great PDF collaborator! }, }; /** * Associate color values to names * * The colorspace parameter is optional. When it is missing, the colorspace * is automatically determined by the given color value. Note that the special * PDF color space called 'separation' may also be used. The color value is then * treated as the alternative color when the named 'separation' color is unavailable. * * If the 'name' parameter is '!load', the second parameter is the name of a JSON * formatted file containing a formatted list of defined colors associated with the * color spaces rgb, cmyk, gray, or separation (think PANTONE color definitions). * This file will be merged with existing set of known colors. The color values * must be specified as hex values. * * For example, * { * 'rgb': {'purple':'ff00ff', 'red':'#ff0000'}, * 'cmyk': {'cyan':'ff000000', 'magenta':'%0,100,0,0'}, * 'gray': {'grey':'#33'} * } * * @name chroma * @function * @memberof Recipe * @param {string} name - the name to be associated to given color value, or '!load' * @param {string|number[]} value - the color value (HexColor, DecimalColor, or PercentColor), or name of '!load' file * @param {string} colorspace - one of the followning: 'rgb', 'cmyk', 'gray', 'separation'; */ exports.chroma = function chroma(name, value, colorspace = "") { if (name) { if (name === "!load") { let newColors = JSON.parse(fs.readFileSync(value)); // Add new colors to existing colorspaces for (let cs in newColors) { if (this.knownColors[cs]) { Object.assign(this.knownColors[cs], newColors[cs]); } else { throw new Error(`Unrecognized colorspace: ${cs}`); } } } else { if (Array.isArray(value)) { value = arrayToHex(value); } else if (value.startsWith("%")) { value = percentToHex(value.replace("%", "")); } else { value = value.replace("#", ""); } // Only deal with valid hex codes from the // device colorspaces gray, rgb, and cmyk. if (![2, 6, 8].includes(value.toString().length)) { throw new Error( "Color value has incorrect size for gray, rgb, or cmyk colorspaces" ); } // Determine colorspace by length of given input // value when colorspace not provided in call. if (colorspace === "") { const colorSpaces = { 2: "gray", 6: "rgb", 8: "cmyk" }; colorspace = colorSpaces[`${value.length}`]; } else if (!["rgb", "cmyk", "gray", "separation"].includes(colorspace)) { throw new Error(`Unknown colorspace: ${colorspace}.`); } if (colorspace) { this.knownColors[colorspace][name] = value; } } } return this; }; function createColorSpaces(self, colorName, color) { const deviceCS = { 1: "DeviceGray", 3: "DeviceRGB", 4: "DeviceCMYK" }; const altCS = deviceCS[`${color.length}`]; this.colorSpaces = this.colorSpaces || {}; this.colorSpaces[altCS] = this.colorSpaces[altCS] || {}; let colorSpaceID = this.colorSpaces[altCS][colorName]; if (!colorSpaceID) { const transformFunction = tintTransform(self, color); self.pauseContext(); const objCxt = self.writer.getObjectsContext(); colorSpaceID = objCxt.startNewIndirectObject(); objCxt .startArray() .writeName("Separation") .writeName(colorName) .writeName(altCS) .writeIndirectObjectReference(transformFunction) .endArray(muhammara.eTokenSeparatorEndLine) .endIndirectObject(); self.resumeContext(); this.colorSpaces[altCS][colorName] = colorSpaceID; } return colorSpaceID; } function tintTransform(self, color) { const rangeCount = color.length; self.pauseContext(); const objCxt = self.writer.getObjectsContext(); const tintFuncID = objCxt.startNewIndirectObject(); const dict = objCxt.startDictionary(); dict.writeKey("FunctionType").writeNumberValue(2).writeKey("Domain"); objCxt.startArray().writeNumber(0.0).writeNumber(1.0).endArray(); dict.writeKey("Range"); objCxt.startArray(); for (let index = 0; index < rangeCount; index++) { objCxt.writeNumber(0.0).writeNumber(1.0); } objCxt.endArray(); dict.writeKey("N"); dict.writeNumberValue(1); dict.writeKey("C0"); objCxt.startArray(); for (let index = 0; index < rangeCount; index++) { objCxt.writeNumber(0.0); } objCxt.endArray(); dict.writeKey("C1"); objCxt.startArray(); for (let index = 0; index < rangeCount; index++) { objCxt.writeNumber(color[index]); } objCxt.endArray(); objCxt.endDictionary(dict); objCxt.endIndirectObject(); self.resumeContext(); return tintFuncID; } exports._createExtGStates = function _createExtGStates(value) { this.extGStates = this.extGStates || {}; if (this.extGStates[value]) { return this.extGStates[value]; } const write = (key, value) => { this.pauseContext(); const objCxt = this.writer.getObjectsContext(); const gsId = objCxt.startNewIndirectObject(); const dict = objCxt.startDictionary(); dict.writeKey("type"); dict.writeNameValue("ExtGState"); dict.writeKey(key); objCxt.writeNumber(value); objCxt.endLine(); objCxt.endDictionary(dict); objCxt.endIndirectObject(); // new here [seh] this.resumeContext(); return gsId; }; this.extGStates[value] = { stroke: write("CA", value), fill: write("ca", value), }; return this.extGStates[value]; }; function _defaultColor(colorspace = "rgb") { let defaultColor; switch (colorspace) { case "cmyk": defaultColor = "FF000000"; break; case "gray": defaultColor = "00"; break; case "rgb": default: defaultColor = "1777d1"; break; } return defaultColor; } /** * Convert given color code int color model object * * ColorModel consists of: { * color: number, * colorspace: string {'rgb', 'cmyk', 'gray'}, * (colorspace == 'rgb') r, g, b * (colorspace == 'cmyk') c, m, y, k * (colorspace == 'gray') gray * } * * where r,g,b,c,m,y,k,gray are all numbers between 0 and 1 * * @param {string} code the color encoding as HexColor * @param {string} colorspace the name of the colorspace of given color code * @param {string} colorName the name to be associated with given color code * @returns {any} the color model */ function toColorModel(self, code, colorspace, colorName) { const cmodel = {}; let color = hexToArray(code); cmodel.color = parseInt(code, 16); // The initial decider of color space is length of given 'code'. switch (color.length) { default: color = hexToArray(_defaultColor("rgb")); // purposely want to fall through to 'rgb' case below. case 3: cmodel.colorspace = "rgb"; cmodel.r = color[0]; cmodel.g = color[1]; cmodel.b = color[2]; break; case 4: cmodel.colorspace = "cmyk"; cmodel.c = color[0]; cmodel.m = color[1]; cmodel.y = color[2]; cmodel.k = color[3]; break; case 1: cmodel.colorspace = "gray"; cmodel.gray = color[0]; break; } // When creating a separation color space, // use the colorspace from above as the // alternative color transformation when // the named color is unavailable. if (colorspace === "separation" && colorName !== "") { cmodel.colorspace = colorspace; cmodel.colorName = colorName; cmodel.colorspaceId = createColorSpaces(self, colorName, color); } return cmodel; } /** * Convert percentage string into hex string (x / 100 * 255) * * @param {string} code numbers separated by commas with values ranging between 0-100. * @returns {string} massaged hexadecimal string that can be used as input to hexToArray. */ function percentToHex(code) { return arrayToHex(code.split(",").map((x) => Math.round(x * 2.55))); } /** * Transform color code into numeric value or colorModel * * @param code color specification in form of HexColor (string, begins with '#'), * DecimalColor (1, 3, or 4 element array with values between 0-255), * PercentColor (string, begins with '%' followed by values separated * by commas with values between 0-100) */ exports._transformColor = function _transformColor(code = "", opt = {}) { this.knownColors = this.knownColors || {}; let colorspace = opt.colorspace || "rgb"; let wantColorModel = opt.wantColorModel || false; let colorName = opt.colorName || ""; let defaultColor = _defaultColor(colorspace); let transformation; if (Array.isArray(code)) { code = arrayToHex(code); } else if (code.startsWith("#")) { code = code.replace("#", ""); } else if (code.startsWith("%")) { code = percentToHex(code.replace("%", "")); } else if (code !== "") { let color = this.knownColors[colorspace][code]; // assuming code is a color name if (!color) { color = ""; code = defaultColor; } else { colorName = code; // The following handles known colors in hex form, // with or without initial '#' and percent form. if (color.startsWith("#")) { code = color.replace("#", ""); } else if (color.startsWith("%")) { code = percentToHex(color.replace("%", "")); } else { code = color; } } } // When colorspace is not explicitly given, // use size of value to determine colorspace. if (!opt.colorspace) { colorspace = { 2: "gray", 6: "rgb", 8: "cmyk" }[`${code.length}`] || "rgb"; defaultColor = _defaultColor(colorspace); } // Suppply default color: // when colorspace is given and given color code does not have appropriate length, or // when colorspace is missing, verify allowable hex value sizes for rgb, cmyk, or gray. if ( (["rgb", "cmyk", "gray"].includes(colorspace) && code.length != defaultColor.length) || ![2, 6, 8].includes(code.toString().length) ) { code = defaultColor; } if (wantColorModel) { transformation = toColorModel(this, code, colorspace, colorName); if (colorName && !this.knownColors[colorspace][colorName]) { this.chroma(colorName, code, colorspace); } } else { transformation = parseInt(`0x${code.toUpperCase()}`, 16); } return transformation; }; function arrayToHex(color = []) { let code = ""; color.forEach((item) => { let hex = item.toString(16); hex = hex.length == 1 ? "0" + hex : hex; code += hex; }); return code; } function hexToArray(hex) { const result = /^#?([a-f\d]{2})([a-f\d]{2})?([a-f\d]{2})?([a-f\d]{2})?$/i.exec(hex); return result.reduce((array, item, index) => { if (index >= 1 && index <= hex.length / 2) { array.push(parseInt(item, 16) / 255); } return array; }, []); } exports._colorNumberToRGB = (bigint) => { if (!bigint) { return { r: 0, g: 0, b: 0, }; } else { return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255, }; } };