From 1a964deb5121628600ee94c45bf36db5aea11712 Mon Sep 17 00:00:00 2001 From: metafloor Date: Fri, 1 Apr 2022 09:17:41 -0600 Subject: [PATCH] Added ACS encoding for #7 --- README.md | 45 ++++++++++++++------ package.json | 1 + zpl-image.html | 76 ++++++++++++++++++++++++++++++---- zpl-image.js | 110 ++++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 212 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index e7613d5..c4a0a7b 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,15 @@ # zpl-image -A pure javascript module that converts images to Z64-encoded GRF bitmaps for use with ZPL. -Works in both node.js and modern browsers. +A pure javascript module that converts images to either Z64-encoded or ACS-encoded GRF bitmaps for use with ZPL. +The term ACS (Alternative Compression Scheme) denotes the run-length compression algorithm described in the section +of the ZPL Reference Manual titled "Alternative Data Compression Scheme". Z64 typically gives better compression +but is not available on all printers (especially older ones). The ACS encoding should work on any printer made +since the mid 90s, maybe earlier. This module provides the following features: + - Works in both node.js and modern browsers. - Converts the image to grayscale, then applies a user-supplied blackness threshold to decide which pixels are black. - Optionally removes any empty/white space around the edges of the image. @@ -13,8 +17,8 @@ This module provides the following features: is often necessary as ZPL does not provide the ability to rotate an image during formatting. - Converts the monochrome image to a GRF bitmap. - - Uses zlib in node.js or pako.js in the browser to compress the GRF bitmap. - - Encodes the compressed bitmap in base64 and calculates the required CRC16 checksum. + - Converts the GRF bitmap to either Z64 or ACS encoding. + - For Z64, zlib in node.js or pako.js in the browser is used for compression. The blackness threshold is specified as an integer between 1 and 99 (think of it as a gray percentage). Pixels darker than the gray% are converted to black. The default is 50. @@ -43,12 +47,12 @@ Included with this module is the file `zpl-image.html`. You can run it directly from the browser using the `file://` scheme. It lets you drag and drop an image and then interactively adjust the blackness threshold and rotation. -When you are satisfied with the results, you can copy the generated ZPL to the clipboard. -The ZPL will have the following format: +When you are satisfied with the results, select either Z64 or ACS encoding and +click the clipboard icon to copy the ZPL. The ZPL will have the following format: ``` ^FX filename.ext (WxHpx, X-Rotate, XX% Black)^FS -^GFA,grflen,grflen,rowlen,:Z64:...base64...encoding...:crc16 +^GFA,grflen,grflen,rowlen,...ASCII-armored-encoding... ``` `^FX ... ^FS` is a ZPL comment. @@ -56,9 +60,9 @@ The ZPL will have the following format: `^GF` is the ZPL command for use-once image rendering (that is, the image is not saved to the printer for later recall by other label formats). -The rendered image displayed on the page is the actual Z64 data decoded and then drawn +The rendered image displayed on the page is the actual data decoded and then drawn to a canvas. If you are interested in that bit of functionality, look for `z64ToCanvas` -in the html file. +and `acsToCanvas` in the `zpl-image.html` file. ## Generic Browser Usage @@ -97,6 +101,10 @@ by both node.js and `imageToZ64()`. See the node.js section for more details. let res = rgbaToZ64(rgba, width, { black:55, rotate:'I' }); ``` +The same interfaces exist for ACS encoding, using the functions `imageToACS()` and +`rgbaToACS()`. The returned object from each function is identical to the above, with +the exception that the encoded text is in the `acs` property instead of `z64`. + ## RequireJS Browser Usage This is untested but the module exports are wrapped in a UMD, so in theory you @@ -104,22 +112,32 @@ should be able to use this with RequireJS. The exports are the same as with the generic browser usage: ```javascript +// Use the Z64 interface const { imageToZ64, rgbaToZ64 } = require("zpl-image"); + +// Or the ACS interface +const { imageToACS, rgbaToACS } = require("zpl-image"); ``` ## Node.js Usage -The return from `require("zpl-image")` is currently a single named function -`rgbaToZ64()`. +The exports from `require("zpl-image")` are the functions `rgbaToZ64()` and +`rgbaToACS()`. ```javascript +// The Z64 interface const rgbaToZ64 = require("zpl-image").rgbaToZ64; + +// The ACS interface +const rgbaToACS = require("zpl-image").rgbaToACS; + ``` -The method takes two or three parameters: +Both methods take two or three parameters: ``` rgbaToZ64(rgba, width [, opts]) +rgbaToACS(rgba, width [, opts]) ``` `rgba` is an array-like object with length equal to `width * height * 4`. @@ -143,6 +161,9 @@ examples showing three different image modules: - [omggif](https://www.npmjs.com/package/omggif) - [jpeg-js](https://www.npmjs.com/package/jpeg-js) +All of the following examples show Z64 encoding but can be switched to ACS +by simply renaming `Z64` to `ACS`. + ## pngjs (PNG Conversion) [pngjs](https://www.npmjs.com/package/pngjs) diff --git a/package.json b/package.json index 576bf34..9d90e93 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.2", "description": "A pure javascript module that converts PNG, JPEG, and GIF to Z64-encoded GRF bitmaps for use with ZPL.", "main": "zpl-image.js", + "module": "zpl-image-esm.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, diff --git a/zpl-image.html b/zpl-image.html index fa2eb4c..db157e2 100644 --- a/zpl-image.html +++ b/zpl-image.html @@ -103,6 +103,8 @@ document.getElementById('rotI').addEventListener('click', convertImage, false); document.getElementById('black').addEventListener('input', convertImage, false); document.getElementById('notrim').addEventListener('click', convertImage, false); + document.getElementById('acscomp').addEventListener('click', convertImage, false); + document.getElementById('z64comp').addEventListener('click', convertImage, false); document.getElementById('copyzpl').addEventListener('click', copyZPL, false); // Setup the drop target @@ -163,25 +165,34 @@ } let black = +document.getElementById('black').value || 50; let rotrad = document.querySelector('input[name=rot]:checked'); + let comprad = document.querySelector('input[name=compress]:checked'); let rot = rotrad && rotrad.value || 'N'; + let comp = comprad && comprad.value || 'Z64'; let notrim = document.getElementById('notrim').checked; // Get the image and convert to Z64 let img = document.getElementById('image'); - let res = imageToZ64(img, { black:black, rotate:rot, notrim:notrim }); + let res; + let bmp; // actually a canvas object + if (comp == 'Z64') { + res = imageToZ64(img, { black:black, rotate:rot, notrim:notrim }); + bmp = z64ToCanvas(res.z64, res.rowlen); + } else { + res = imageToACS(img, { black:black, rotate:rot, notrim:notrim }); + bmp = acsToCanvas(res.acs, res.rowlen); + } - // Draw the Z64-encoded image to our canvas - let grf = z64ToCanvas(res.z64, res.rowlen); + // Draw the image to our canvas let cvs = document.getElementById('canvas'); - cvs.width = grf.width; - cvs.height = grf.height; - cvs.getContext('2d').drawImage(grf, 0, 0); + cvs.width = bmp.width; + cvs.height = bmp.height; + cvs.getContext('2d').drawImage(bmp, 0, 0); // Create the ZPL with a source comment document.getElementById('zpltext').value = '^FX ' + _filename + ' (' + res.width + 'x' + res.height + 'px, ' + rot + '-Rotate, ' + black + '% Black)^FS\n' + - '^GFA,' + res.length + ',' + res.length + ',' + res.rowlen + ',' + res.z64 + '\n'; + '^GFA,' + res.length + ',' + res.length + ',' + res.rowlen + ',' + (res.z64||res.acs) + '\n'; } function copyZPL() { let ta = document.getElementById('zpltext'); @@ -227,6 +238,52 @@ ctx.putImageData(bmap, 0, 0); return cvs; } +function acsToCanvas(acs, rowl) { + let hex = acs.replace(/[g-zG-Y]+([0-9a-fA-F])/g, ($0, $1) => { + let rep = 0; + for (let i = 0, l = $0.length-1; i < l; i++) { + let cd = $0.charCodeAt(i); + if (cd < 90) { // 'Z' + rep += cd - 70; + } else { + rep += (cd - 102) * 20; + } + } + return $1.repeat(rep); + }); + + let bytes = Array(hex.length/2); + for (let i = 0, l = hex.length; i < l; i += 2) { + bytes[i>>1] = parseInt(hex.substr(i,2), 16); + } + + let l = bytes.length; + let w = rowl * 8; // rowl is in bytes + let h = ~~(l / rowl); + + // Render the GRF to a canvas + let cvs = document.createElement('canvas'); + cvs.width = w; + cvs.height = h; + + let ctx = cvs.getContext('2d'); + let bmap = ctx.getImageData(0, 0, w, h); + let data = bmap.data; + let offs = 0; + for (let i = 0; i < l; i++) { + let byte = bytes[i]; + for (let bit = 0x80; bit; bit = bit >>> 1, offs += 4) { + if (bit & byte) { + data[offs] = 0; + data[offs+1] = 0; + data[offs+2] = 0; + data[offs+3] = 255; // Fully opaque + } + } + } + ctx.putImageData(bmap, 0, 0); + return cvs; +} @@ -252,6 +309,11 @@ 1..99 + ZPL Format + + diff --git a/zpl-image.js b/zpl-image.js index 43ad896..7dc8b49 100644 --- a/zpl-image.js +++ b/zpl-image.js @@ -16,6 +16,17 @@ const zlib = typeof process == 'object' && typeof process.release == 'object' && process.release.name == 'node' ? require('zlib') : null; +const hexmap = (()=> { + let arr = Array(256); + for (let i = 0; i < 16; i++) { + arr[i] = '0' + i.toString(16); + } + for (let i = 16; i < 256; i++) { + arr[i] = i.toString(16); + } + return arr; +})(); + // DOM-specialized version for browsers. function imageToZ64(img, opts) { // Draw the image to a temp canvas so we can access its RGBA data @@ -30,6 +41,19 @@ function imageToZ64(img, opts) { return rgbaToZ64(pixels.data, pixels.width, opts); } +// DOM-specialized version for browsers. +function imageToACS(img, opts) { + // Draw the image to a temp canvas so we can access its RGBA data + let cvs = document.createElement('canvas'); + let ctx = cvs.getContext('2d'); + + cvs.width = +img.width || img.offsetWidth; + cvs.height = +img.height || img.offsetHeight; + ctx.drawImage(img, 0, 0); + + let pixels = ctx.getImageData(0, 0, cvs.width, cvs.height); + return rgbaToACS(pixels.data, pixels.width, opts); +} // Uses zlib on node.js, pako.js in the browser. // @@ -86,6 +110,90 @@ function rgbaToZ64(rgba, width, opts) { }; } +// Implements the Alternative Data Compression Scheme as described in the ref manual. +// +// `rgba` can be a Uint8Array or Buffer, or an Array of integers between 0 and 255. +// `width` is the image width, in pixels +// `opts` is an options object: +// `black` is the blackness percent between 1..99, default 50. +// `rotate` is one of: +// 'N' no rotation (default) +// 'L' rotate 90 degrees counter-clockwise +// 'R' rotate 90 degrees clockwise +// 'I' rotate 180 degrees (inverted) +// 'B' same as 'L' +function rgbaToACS(rgba, width, opts) { + opts = opts || {}; + width = width|0; + if (!width || width < 0) { + throw new Error('Invalid width'); + } + let height = ~~(rgba.length / width / 4); + + // Create a monochome image, cropped to remove padding. + // The return is a Uint8Array with extra properties width and height. + let mono = monochrome(rgba, width, height, +opts.black || 50, opts.notrim); + + let buf; + switch (opts.rotate) { + case 'R': buf = right(mono); break; + case 'B': + case 'L': buf = left(mono); break; + case 'I': buf = invert(mono); break; + default: buf = normal(mono); break; + } + + // Encode in hex and apply the "Alternative Data Compression Scheme" + // + // G H I J K L M N O P Q R S T U V W X Y + // 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 + // + // g h i j k l m n o p q r s t u v w x y z + // 20 40 60 80 100 120 140 160 180 200 220 240 260 280 300 320 340 360 380 400 + // + let imgw = buf.width; + let imgh = buf.height; + let rowl = ~~((imgw + 7) / 8); + + let hex = ''; + for (let i = 0, l = buf.length; i < l; i++) { + hex += hexmap[buf[i]]; + } + let acs = ''; + let re = /([0-9a-fA-F])\1{2,}/g; + let match = re.exec(hex); + let offset = 0; + while (match) { + acs += hex.substring(offset, match.index); + let l = match[0].length; + while (l >= 400) { + acs += 'z'; + l -= 400; + } + if (l >= 20) { + acs += '_ghijklmnopqrstuvwxy'[((l / 20)|0)]; + l = l % 20; + } + if (l) { + acs += '_GHIJKLMNOPQRSTUVWXY'[l]; + } + acs += match[1]; + offset = re.lastIndex; + match = re.exec(hex); + } + acs += hex.substr(offset); + + // Example usage of the return value `rv`: + // '^GFA,' + rv.length + ',' + rv.length + ',' + rv.rowlen + ',' + rv.acs + return { + length: buf.length, // uncompressed number of bytes + rowlen: rowl, // number of packed bytes per row + width: imgw, // rotated image width in pixels + height: imgh, // rotated image height in pixels + acs: acs, + }; +} + // Normal, unrotated case function normal(mono) { let width = mono.width; @@ -330,5 +438,5 @@ function crc16(s) { return '0000'.substr(crc.length) + crc; } -return zlib ? { rgbaToZ64 } : { rgbaToZ64, imageToZ64 }; +return zlib ? { rgbaToZ64, rgbaToACS } : { rgbaToZ64, rgbaToACS, imageToZ64, imageToACS }; }));