Skip to content

Commit

Permalink
Added ACS encoding for #7
Browse files Browse the repository at this point in the history
  • Loading branch information
metafloor committed Apr 1, 2022
1 parent fecc6b6 commit 1a964de
Show file tree
Hide file tree
Showing 4 changed files with 212 additions and 20 deletions.
45 changes: 33 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@

# 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.
- Optionally rotates the image to one of the orthogonal angles. This step
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.
Expand Down Expand Up @@ -43,22 +47,22 @@ 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.

`^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

Expand Down Expand Up @@ -97,29 +101,43 @@ 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
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`.
Expand All @@ -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)
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
76 changes: 69 additions & 7 deletions zpl-image.html
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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');
Expand Down Expand Up @@ -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;
}
</script>
</head>
<body>
Expand All @@ -252,6 +309,11 @@
<span>1..99</span>
<td><label for="notrim">
<input type="checkbox" id="notrim" value="Y">&nbsp;No&nbsp;Trim</label>
<tr><th>ZPL Format<td colspan=2>
<label for="z64comp"><input type="radio" name="compress" value="Z64"
id="z64comp" checked>Z64</label>
<label for="acscomp"><input type="radio" name="compress" value="ACS"
id="acscomp">ACS</label>
</table>
</div>
<textarea rows=24 cols=112 id="zpltext" readonly></textarea>
Expand Down
110 changes: 109 additions & 1 deletion zpl-image.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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.
//
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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 };
}));

0 comments on commit 1a964de

Please sign in to comment.