UNPKG

@replytechnologies/zpl-image-convert

Version:
516 lines (442 loc) 12.7 kB
const getPixels = require("get-pixels"); const zlib = require("zlib"); const mapCode = { // normal 1: "G", 2: "H", 3: "I", 4: "J", 5: "K", 6: "L", 7: "M", 8: "N", 9: "O", 10: "P", 11: "Q", 12: "R", 13: "S", 14: "T", 15: "U", 16: "V", 17: "W", 18: "X", 19: "Y", 20: "g", 40: "h", 60: "i", 80: "j", 100: "k", 120: "l", 140: "m", 160: "n", 180: "o", 200: "p", 220: "q", 240: "r", 260: "s", 280: "t", 300: "u", 320: "v", 340: "w", 360: "x", 380: "y", 400: "z", }; const pivotedMapCode = { // pivoted G: 1, H: 2, I: 3, J: 4, K: 5, L: 6, M: 7, N: 8, O: 9, P: 10, Q: 11, R: 12, S: 13, T: 14, U: 15, V: 16, W: 17, X: 18, Y: 19, g: 20, h: 40, i: 60, j: 80, k: 100, l: 120, m: 140, n: 160, o: 180, p: 200, q: 220, r: 240, s: 260, t: 280, u: 300, v: 320, w: 340, x: 360, y: 380, z: 400, }; const crcTable = [ 0x0000, 0x1021, 0x2042, 0x3063, 0x4084, 0x50a5, 0x60c6, 0x70e7, 0x8108, 0x9129, 0xa14a, 0xb16b, 0xc18c, 0xd1ad, 0xe1ce, 0xf1ef, 0x1231, 0x0210, 0x3273, 0x2252, 0x52b5, 0x4294, 0x72f7, 0x62d6, 0x9339, 0x8318, 0xb37b, 0xa35a, 0xd3bd, 0xc39c, 0xf3ff, 0xe3de, 0x2462, 0x3443, 0x0420, 0x1401, 0x64e6, 0x74c7, 0x44a4, 0x5485, 0xa56a, 0xb54b, 0x8528, 0x9509, 0xe5ee, 0xf5cf, 0xc5ac, 0xd58d, 0x3653, 0x2672, 0x1611, 0x0630, 0x76d7, 0x66f6, 0x5695, 0x46b4, 0xb75b, 0xa77a, 0x9719, 0x8738, 0xf7df, 0xe7fe, 0xd79d, 0xc7bc, 0x48c4, 0x58e5, 0x6886, 0x78a7, 0x0840, 0x1861, 0x2802, 0x3823, 0xc9cc, 0xd9ed, 0xe98e, 0xf9af, 0x8948, 0x9969, 0xa90a, 0xb92b, 0x5af5, 0x4ad4, 0x7ab7, 0x6a96, 0x1a71, 0x0a50, 0x3a33, 0x2a12, 0xdbfd, 0xcbdc, 0xfbbf, 0xeb9e, 0x9b79, 0x8b58, 0xbb3b, 0xab1a, 0x6ca6, 0x7c87, 0x4ce4, 0x5cc5, 0x2c22, 0x3c03, 0x0c60, 0x1c41, 0xedae, 0xfd8f, 0xcdec, 0xddcd, 0xad2a, 0xbd0b, 0x8d68, 0x9d49, 0x7e97, 0x6eb6, 0x5ed5, 0x4ef4, 0x3e13, 0x2e32, 0x1e51, 0x0e70, 0xff9f, 0xefbe, 0xdfdd, 0xcffc, 0xbf1b, 0xaf3a, 0x9f59, 0x8f78, 0x9188, 0x81a9, 0xb1ca, 0xa1eb, 0xd10c, 0xc12d, 0xf14e, 0xe16f, 0x1080, 0x00a1, 0x30c2, 0x20e3, 0x5004, 0x4025, 0x7046, 0x6067, 0x83b9, 0x9398, 0xa3fb, 0xb3da, 0xc33d, 0xd31c, 0xe37f, 0xf35e, 0x02b1, 0x1290, 0x22f3, 0x32d2, 0x4235, 0x5214, 0x6277, 0x7256, 0xb5ea, 0xa5cb, 0x95a8, 0x8589, 0xf56e, 0xe54f, 0xd52c, 0xc50d, 0x34e2, 0x24c3, 0x14a0, 0x0481, 0x7466, 0x6447, 0x5424, 0x4405, 0xa7db, 0xb7fa, 0x8799, 0x97b8, 0xe75f, 0xf77e, 0xc71d, 0xd73c, 0x26d3, 0x36f2, 0x0691, 0x16b0, 0x6657, 0x7676, 0x4615, 0x5634, 0xd94c, 0xc96d, 0xf90e, 0xe92f, 0x99c8, 0x89e9, 0xb98a, 0xa9ab, 0x5844, 0x4865, 0x7806, 0x6827, 0x18c0, 0x08e1, 0x3882, 0x28a3, 0xcb7d, 0xdb5c, 0xeb3f, 0xfb1e, 0x8bf9, 0x9bd8, 0xabbb, 0xbb9a, 0x4a75, 0x5a54, 0x6a37, 0x7a16, 0x0af1, 0x1ad0, 0x2ab3, 0x3a92, 0xfd2e, 0xed0f, 0xdd6c, 0xcd4d, 0xbdaa, 0xad8b, 0x9de8, 0x8dc9, 0x7c26, 0x6c07, 0x5c64, 0x4c45, 0x3ca2, 0x2c83, 0x1ce0, 0x0cc1, 0xef1f, 0xff3e, 0xcf5d, 0xdf7c, 0xaf9b, 0xbfba, 0x8fd9, 0x9ff8, 0x6e17, 0x7e36, 0x4e55, 0x5e74, 0x2e93, 0x3eb2, 0x0ed1, 0x1ef0, ]; async function encode(file, options) { options = options || {}; options.method = options.method || "Z64"; options.mimeType = options.mimeType || null; options.threshold = options.threshold || 0x80; options.luminance = options.luminance || { r: 0.2126, g: 0.7152, b: 0.0722, }; const acceptedMethods = ["Z64", "ASCII"]; if (!acceptedMethods.includes(options.method)) { throw new Error( `Method '${options.method}' is not supported (${acceptedMethods.join( ", " )})` ); } let image = null; if (file.bitmap) { // file originated from Jimp or already processed into pixels image = file; } else { image = await getImagePixels(file, options); } const monochromeImage = convertImageToMonochrome(image, options); let data = null; if (options.method === "Z64") { data = encodeZ64(monochromeImage.buffer, monochromeImage.width); } if (options.method === "ASCII") { data = encodeASCII(monochromeImage.buffer, monochromeImage.width); } return `^GFA,${monochromeImage.buffer.length},${ monochromeImage.buffer.length },${monochromeImage.width / 8},${data}^FS`; } async function getImagePixels(file, options) { return new Promise((resolve, reject) => { getPixels(file, options.mimeType, (error, pixels) => { if (error) { reject(error); } else { const bitmap = { width: pixels.shape[0], height: pixels.shape[1], channels: pixels.shape[2], }; const result = { data: pixels.data, getPixelColor: (x, y) => { const index = (y * bitmap.width + x) * bitmap.channels; return { r: pixels.data[index], g: bitmap.channels > 1 ? pixels.data[index + 1] : 0, b: bitmap.channels > 2 ? pixels.data[index + 2] : 0, a: bitmap.channels > 3 ? pixels.data[index + 3] : 0, }; }, bitmap: bitmap, }; resolve(result); } }); }); } function convertImageToMonochrome(image, options) { // image width must be multiples of 8 const imageWidth = ~~((image.bitmap.width + 7) / 8) * 8; let buffer = new Uint8Array((image.bitmap.height * imageWidth) / 8); let currentValue = 0; let bitCounter = 0; let index = 0; for (let y = 0; y < image.bitmap.height; y++) { for (let x = 0; x < imageWidth; x++) { let value = 0; if (x < image.bitmap.width) { let pixel = image.getPixelColor(x, y); if (typeof pixel == "number") { // pixel value is a number pixel >>>= 0; pixel = { r: (pixel >> 24) & 0xff, g: (pixel >> 16) & 0xff, b: (pixel >> 8) & 0xff, a: pixel & 0xff, }; } const alpha = pixel.a / 255; const luminanceValue = (pixel.r * options.luminance.r + pixel.g * options.luminance.g + pixel.b * options.luminance.b) * alpha + 255 * (1 - alpha); value = luminanceValue < options.threshold ? 1 : 0; } currentValue = currentValue | (value << (7 - bitCounter)); bitCounter++; if (bitCounter == 8) { buffer[index] = currentValue; currentValue = 0; bitCounter = 0; index++; } } } return { buffer: Buffer.from(buffer), width: imageWidth, height: image.bitmap.height, }; } function encodeASCII(buffer, width) { let lines = []; let currentLine = ""; const lineLength = width / 8; for (let index = 0; index < buffer.length; index++) { currentLine += buffer[index].toString("16").padStart(2, "0"); if ((index + 1) % lineLength == 0) { lines.push(currentLine); currentLine = ""; } } const newLines = []; // process duplicate lines for (let index = 0; index < lines.length; index++) { const line = lines[index]; if (line === lines[index - 1]) { newLines[index] = ":"; } else { newLines[index] = line; } } lines = newLines; // process trailing zeros for (let index = 0; index < lines.length; index++) { lines[index] = processAsciiLine(lines[index]); } // join lines into single string const line = lines.join(""); let data = ""; const nonCountingCharacters = [",", ":", "!"]; let currentCharacter = null; let counter = 0; let characterIndex = 0; while (characterIndex <= line.length) { const character = line[characterIndex]; const isNonCountingCharacter = nonCountingCharacters.includes(currentCharacter); if (!currentCharacter) { currentCharacter = character; counter = 1; } else { if (character == currentCharacter && !isNonCountingCharacter) { counter++; } else { if (isNonCountingCharacter || counter == 1) { data += currentCharacter; } else { const mapCodeValues = getMapCodeValues(counter); data += `${mapCodeValues}${currentCharacter}`; } currentCharacter = null; continue; } } characterIndex++; } return data; } function processAsciiLine(line) { line = line.toUpperCase(); const zeroRegex = /0+$/; line = line.replace(zeroRegex, ","); return line; } function getMapCodeValues(counter) { if (mapCode[counter]) { return mapCode[counter]; } let mapCodeValues = ""; while (counter > 20) { mapCodeValues += getMapCodeValues(~~(counter / 20) * 20); counter = counter % 20; } mapCodeValues += mapCode[counter]; return mapCodeValues; } function encodeZ64(buffer) { const base64Value = zlib.deflateSync(buffer).toString("base64"); const crc16Value = calculateCRC(base64Value); return `:Z64:${base64Value}:${crc16Value}`; } function calculateCRC(input) { let crc = 0; for (let index = 0; index < input.length; index++) { const character = input.charCodeAt(index); if (character > 255) { throw new RangeError(); } const crcIndex = (character ^ (crc >> 8)) & 0xff; crc = crcTable[crcIndex] ^ (crc << 8); } crc = (crc & 0xffff).toString(16).toLowerCase(); return crc.padStart(4, "0"); } function decode(text) { text = text.trim(); if (!text.startsWith("^GFA") && !text.startsWith("A")) { throw new Error("Unsupported encoding"); } // trim ^GF if (text.startsWith("^GF")) { text = text.substring(3); } // trim trailing '^FS' if (text.endsWith("^FS")) { text = text.substring(0, text.length - 3); } // a: compression type (A, B, C) let commaIndex = text.indexOf(","); const a = text.substring(0, commaIndex); text = text.substring(commaIndex + 1); // b: binary byte count commaIndex = text.indexOf(","); const b = text.substring(0, commaIndex); text = text.substring(commaIndex + 1); // c: graphic field count commaIndex = text.indexOf(","); const c = text.substring(0, commaIndex); text = text.substring(commaIndex + 1); // d: bytes per row commaIndex = text.indexOf(","); const d = text.substring(0, commaIndex); const data = text.substring(commaIndex + 1); let buffer = null; const width = d * 8; const height = c / d; if (data.startsWith(":Z64:")) { buffer = decodeZ64(data); } else { buffer = decodeASCII(data, c, d); } return { width, height, buffer, getPixelBit: (x, y) => { const byteIndex = y * (width / 8) + ~~(x / 8); const byte = buffer[byteIndex]; const bit = (byte >> (7 - (x % 8))) & 0x01; return bit; }, }; } function decodeZ64(data) { // trim :Z64: data = data.substring(5); // trim trailing crc data = data.substring(0, data.length - 5); const deflatedData = Buffer.from(data, "base64"); const buffer = zlib.inflateSync(deflatedData); return buffer; } function decodeASCII(data, size, lineByteCount) { const buffer = new Uint8Array(size); const lineWordCount = lineByteCount * 2; // inflate data from map codes let inflatedData = ""; let index = 0; while (index < data.length) { let character = data[index++]; if (pivotedMapCode[character]) { let code = ""; while (pivotedMapCode[character]) { code += character; character = data[index++]; } const multiplier = getMapCodeCount(code); inflatedData += new Array(multiplier + 1).join(character); } else { inflatedData += character; } } // expand shortened data rows let expandedData = ""; index = 0; while (index < inflatedData.length) { let character = inflatedData[index++]; let remainingLength = lineWordCount - (expandedData.length % lineWordCount); if (character == ",") { expandedData += new Array(remainingLength + 1).join("0"); } else if (character == "!") { expandedData += new Array(remainingLength + 1).join("F"); } else if (character == ":") { expandedData += expandedData.substring( expandedData.length - lineWordCount, expandedData.length ); } else { expandedData += character; } } // convert data into buffer let bufferIndex = 0; index = 0; while (index < expandedData.length) { let character = expandedData[index++]; let nextCharacter = expandedData[index++]; buffer[bufferIndex++] = Number.parseInt(character + nextCharacter, 16); } return buffer; } function getMapCodeCount(code) { let value = 0; for (let index = 0; index < code.length; index++) { value += pivotedMapCode[code[index]]; } return value; } module.exports = { encode, decode, };