= '0' extends T ? 1 : T extends `${infer P}%` ? Less100 : never;
-
-type IsColorValue = IsPercent | Color255;
-
-type RGB = T extends `rgb(${infer R},${infer G},${infer B})`
- ? '111' extends `${IsColorValue>}${IsColorValue>}${IsColorValue>}`
- ? T
- : never
- : never;
-
-type Opacity = IsDecNumber | IsPercent;
-
-type RGBA = T extends `rgba(${infer R},${infer G},${infer B},${infer O})`
- ? '1111' extends `${IsColorValue>}${IsColorValue>}${IsColorValue<
- Trim
- >}${Opacity>}`
- ? T
- : never
- : never;
-
-type HSL = T extends `hsl(${infer H},${infer S},${infer L})`
- ? `111` extends `${Degree>}${IsPercent>}${IsPercent>}`
- ? T
- : never
- : never;
-
-type HSLA = T extends `hsla(${infer H},${infer S},${infer L},${infer O})`
- ? `1111` extends `${Degree>}${IsPercent>}${IsPercent>}${Opacity<
- Trim
- >}`
- ? T
- : never
- : never;
-
-type ColorValue = HexColor | RGB | RGBA | HSL | HSLA;
-
-/**
- * A CSS-property compatible color type. Used for properties which will end up
- * as a CSS property eventually.
- *
- * @see https://stackoverflow.com/a/68068969/1309423
- */
-export type Color =
- | ColorValue
- | ColorKeyword
- | 'currentColor'
- | 'transparent'
- | 'inherit';
diff --git a/test/Documents/BitmapGRF.test.ts b/src/Documents/BitmapGRF.test.ts
similarity index 85%
rename from test/Documents/BitmapGRF.test.ts
rename to src/Documents/BitmapGRF.test.ts
index 658c34c..f8bed9d 100644
--- a/test/Documents/BitmapGRF.test.ts
+++ b/src/Documents/BitmapGRF.test.ts
@@ -1,9 +1,10 @@
-///
+import { expect, describe, it } from 'vitest';
+
import {
BitmapGRF,
DitheringMethod,
- ImageConversionOptions
-} from '../../src/Documents/BitmapGRF.js';
+ type ImageConversionOptions
+} from './BitmapGRF.js';
// Class pulled from jest-mock-canvas which I can't seem to actually import.
class ImageData {
@@ -26,7 +27,24 @@ class ImageData {
return 'srgb' as PredefinedColorSpace;
}
- constructor(arr, w, h) {
+
+ /**
+ * Creates an `ImageData` object from a given `Uint8ClampedArray` and the size of the image it contains.
+ *
+ * @param array A `Uint8ClampedArray` containing the underlying pixel representation of the image.
+ * @param width An `unsigned` `long` representing the width of the image.
+ * @param height An `unsigned` `long` representing the height of the image. This value is optional: the height will be inferred from the array's size and the given width.
+ */
+ constructor(array: Uint8ClampedArray, width: number, height?: number)
+
+ /**
+ * Creates an `ImageData` object of a black rectangle with the given width and height.
+ *
+ * @param width An `unsigned` `long` representing the width of the image.
+ * @param height An `unsigned` `long` representing the height of the image.
+ */
+ constructor(width: number, height: number)
+ constructor(arr: number | Uint8ClampedArray, w: number, h?: number) {
if (arguments.length === 2) {
if (arr instanceof Uint8ClampedArray) {
if (arr.length === 0)
@@ -49,7 +67,7 @@ class ImageData {
this._height = height;
this._data = new Uint8ClampedArray(width * height * 4);
}
- } else if (arguments.length === 3) {
+ } else if (arguments.length === 3 && h !== undefined) {
if (!(arr instanceof Uint8ClampedArray))
throw new TypeError('First argument must be a Uint8ClampedArray when using 3 arguments.');
if (arr.length === 0) throw new RangeError('Source length must be a positive multiple of 4.');
@@ -60,7 +78,7 @@ class ImageData {
if (!Number.isFinite(w)) throw new RangeError('The width is zero or not a number.');
if (w === 0) throw new RangeError('The width is zero or not a number.');
if (arr.length !== w * h * 4)
- throw new RangeError("Source doesn'n contain the exact number of pixels needed.");
+ throw new RangeError("Source doesn't contain the exact number of pixels needed.");
this._width = w;
this._height = h;
this._data = arr;
@@ -74,7 +92,7 @@ global.ImageData = ImageData;
function getImageDataInput(width: number, height: number, fill: number, alpha?: number) {
const arr = new Uint8ClampedArray(width * height * 4);
- if (alpha != null && alpha != fill) {
+ if (alpha != undefined && alpha != fill) {
for (let i = 0; i < arr.length; i += 4) {
arr[i + 0] = fill;
arr[i + 1] = fill;
@@ -112,7 +130,7 @@ describe('RGBA Image Conversion', () => {
it('Should downconvert transparent images correctly', () => {
const imageData = new ImageData(getImageDataInput(8, 1, 0), 8, 1);
const expected = new Uint8Array([(1 << 8) - 1]);
- const { monochromeData, imageWidth, imageHeight, boundingBox } = BitmapGRF['toMonochrome'](
+ const { monochromeData, imageWidth, imageHeight } = BitmapGRF['toMonochrome'](
imageData.data,
imageData.width,
imageData.height,
@@ -133,7 +151,7 @@ describe('RGBA Image Conversion', () => {
it('Should downconvert black images correctly', () => {
const imageData = new ImageData(getImageDataInput(8, 1, 0, 255), 8, 1);
const expected = new Uint8Array([0]);
- const { monochromeData, imageWidth, imageHeight, boundingBox } = BitmapGRF['toMonochrome'](
+ const { monochromeData, imageWidth, imageHeight } = BitmapGRF['toMonochrome'](
imageData.data,
imageData.width,
imageData.height,
@@ -154,7 +172,7 @@ describe('RGBA Image Conversion', () => {
it('Should downconvert white images correctly', () => {
const imageData = new ImageData(getImageDataInput(8, 1, 255), 8, 1);
const expected = new Uint8Array([(1 << 8) - 1]);
- const { monochromeData, imageWidth, imageHeight, boundingBox } = BitmapGRF['toMonochrome'](
+ const { monochromeData, imageWidth, imageHeight } = BitmapGRF['toMonochrome'](
imageData.data,
imageData.width,
imageData.height,
@@ -175,7 +193,7 @@ describe('RGBA Image Conversion', () => {
it('Should downconvert checkered images correctly', () => {
const imageData = new ImageData(getImageDataInputAlternatingDots(8, 1), 8, 1);
const expected = new Uint8Array([85]);
- const { monochromeData, imageWidth, imageHeight, boundingBox } = BitmapGRF['toMonochrome'](
+ const { monochromeData, imageWidth, imageHeight } = BitmapGRF['toMonochrome'](
imageData.data,
imageData.width,
imageData.height,
@@ -196,7 +214,7 @@ describe('RGBA Image Conversion', () => {
it('Should pad and downconvert transparent images correctly', () => {
const imageData = new ImageData(getImageDataInput(5, 1, 0), 5, 1);
const expected = new Uint8Array([(1 << 8) - 1]);
- const { monochromeData, imageWidth, imageHeight, boundingBox } = BitmapGRF['toMonochrome'](
+ const { monochromeData, imageWidth, imageHeight } = BitmapGRF['toMonochrome'](
imageData.data,
imageData.width,
imageData.height,
@@ -218,7 +236,7 @@ describe('RGBA Image Conversion', () => {
const imgWidth = 4;
const imageData = new ImageData(getImageDataInput(imgWidth, 1, 0, 255), imgWidth, 1);
const expected = new Uint8Array([(1 << imgWidth) - 1]);
- const { monochromeData, imageWidth, imageHeight, boundingBox } = BitmapGRF['toMonochrome'](
+ const { monochromeData, imageWidth, imageHeight } = BitmapGRF['toMonochrome'](
imageData.data,
imageData.width,
imageData.height,
@@ -240,7 +258,7 @@ describe('RGBA Image Conversion', () => {
const imgWidth = 4;
const imageData = new ImageData(getImageDataInput(imgWidth, 1, 255), imgWidth, 1);
const expected = new Uint8Array([(1 << 8) - 1]);
- const { monochromeData, imageWidth, imageHeight, boundingBox } = BitmapGRF['toMonochrome'](
+ const { monochromeData, imageWidth, imageHeight } = BitmapGRF['toMonochrome'](
imageData.data,
imageData.width,
imageData.height,
@@ -304,14 +322,12 @@ describe('RGBA Round Trip', () => {
describe('Whitespace Trimming', () => {
it('Should trim to black pixels', () => {
// A single black pixel, surrounded by white on all sides, 10 pixels wide.
- /* eslint-disable prettier/prettier */
const imageData = new ImageData(
new Uint8ClampedArray([
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
- 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
+ 255, 255, 255, 255, 0, 0, 0, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255,
]), 10, 3);
- /* eslint-enable prettier/prettier */
const img = BitmapGRF.fromCanvasImageData(imageData, { trimWhitespace: true });
// Width will always be a multiple of 8 due to byte padding.
diff --git a/src/Documents/BitmapGRF.ts b/src/Documents/BitmapGRF.ts
index 065c385..87ab0bb 100644
--- a/src/Documents/BitmapGRF.ts
+++ b/src/Documents/BitmapGRF.ts
@@ -1,5 +1,4 @@
-/* eslint-disable prettier/prettier */
-import { Percent } from '../NumericRange.js';
+import type { Percent } from '../NumericRange.js';
import { WebZlpError } from '../WebZlpError.js';
/** Padding information for a trimmed image. */
@@ -21,7 +20,7 @@ export interface ImageBoundingBox {
/** Settings for converting an image to a GRF. */
export interface ImageConversionOptions {
- /** The threshold brightness below which to consider a pixel black. Defaults to 90. */
+ /** The threshold brightness below which to consider a pixel black. Defaults to 70. */
grayThreshold?: Percent;
/** Whether to trim whitespace around the image to reduce file size. Trimmed pixels will become padding in the bounding box. */
trimWhitespace?: boolean;
@@ -261,7 +260,7 @@ export class BitmapGRF {
//
// Only supports sRGB as RGBA data.
// if (imageData.colorSpace !== 'srgb') {
- // throw new DocumentValidationError(
+ // throw new TranspileDocumentError(
// 'Unknown color space for given imageData. Expected srgb but got ' +
// imageData.colorSpace
// );
@@ -357,17 +356,17 @@ export class BitmapGRF {
// https://github.com/metafloor/zpl-image/blob/491f4d6887294d71dcfa859957d43b3be28ce1e5/zpl-image.js
// Convert black from percent to 0..255 value
- const threshold = (255 * grayThreshold) / 100;
+ const threshold = (255 * (grayThreshold?? 70)) / 100;
// This is where we'd do some dithering, if we implemented anything other than none.
- if (ditheringMethod != DitheringMethod.none) {
+ if (ditheringMethod !== undefined && ditheringMethod !== DitheringMethod.none) {
throw new WebZlpError(
`Dithering method ${DitheringMethod[ditheringMethod]} is not supported.`
);
}
let minx: number, maxx: number, miny: number, maxy: number;
- if (!trimWhitespace) {
+ if (trimWhitespace !== true) {
minx = miny = 0;
maxx = width - 1;
maxy = height - 1;
diff --git a/src/Documents/Commands.ts b/src/Documents/Commands.ts
index f28a4c4..cff652c 100644
--- a/src/Documents/Commands.ts
+++ b/src/Documents/Commands.ts
@@ -1,573 +1,443 @@
import * as Options from '../Printers/Configuration/PrinterOptions.js';
-import { BitmapGRF, ImageConversionOptions } from './BitmapGRF.js';
+import type { BitmapGRF, ImageConversionOptions } from './BitmapGRF.js';
/** Flags to indicate special operations a command might cause. */
export enum PrinterCommandEffectFlags {
- /** No special side-effects outside of what the command does. */
- none = 0,
- /** The effects of this command cannot be determined automatically. */
- unknownEffects = 1 << 0,
- /** Changes the printer config, necessitating an update of the cached config. */
- altersPrinterConfig = 1 << 1,
- /** Causes the printer motor to engage, even if nothing is printed. */
- feedsLabel = 1 << 2,
- /** Causes the printer to disconnect or otherwise need reconnecting. */
- lossOfConnection = 1 << 3,
- /** Causes something sharp to move */
- actuatesCutter = 1 << 4
+ /** No special side-effects outside of what the command does. */
+ none = 0,
+ /** The effects of this command cannot be determined automatically. */
+ unknownEffects = 1 << 0,
+ /** Changes the printer config, necessitating an update of the cached config. */
+ altersPrinterConfig = 1 << 1,
+ /** Causes the printer motor to engage, even if nothing is printed. */
+ feedsLabel = 1 << 2,
+ /** Causes the printer to disconnect or otherwise need reconnecting. */
+ lossOfConnection = 1 << 3,
+ /** Causes something sharp to move */
+ actuatesCutter = 1 << 4
}
/** A command that can be sent to a printer. */
export interface IPrinterCommand {
- /** Get the display name of this command. */
- get name(): string;
- /** Get the command type of this command. */
- get type(): CommandType;
-
- /** Get the human-readable output of this command. */
- toDisplay(): string;
-
- /** Any effects this command may cause the printer to undergo. */
- readonly printerEffectFlags?: PrinterCommandEffectFlags;
+ /** Get the display name of this command. */
+ readonly name: string;
+ /** Get the command type of this command. */
+ readonly type: CommandType;
+ /** Any effects this command may cause the printer to undergo. */
+ readonly printerEffectFlags: PrinterCommandEffectFlags;
+
+ /** Get the human-readable output of this command. */
+ toDisplay(): string;
}
/** A custom command beyond the standard command set, with command-language-specific behavior. */
export interface IPrinterExtendedCommand extends IPrinterCommand {
- /** The unique identifier for this command. */
- get typeExtended(): symbol;
+ /** The unique identifier for this command. */
+ get typeExtended(): symbol;
- /** Gets the command languages this extended command can apply to. */
- get commandLanguageApplicability(): Options.PrinterCommandLanguage;
+ /** Gets the command languages this extended command can apply to. */
+ get commandLanguageApplicability(): Options.PrinterCommandLanguage;
}
/** List of colors to draw elements with */
export enum DrawColor {
- /** Draw in black */
- black,
- /** Draw in white */
- white
+ /** Draw in black */
+ black,
+ /** Draw in white */
+ white
}
/** Behavior to take for commands that belong inside or outside of a form. */
export enum CommandReorderBehavior {
- /** Perform no reordering, non-form commands will be interpreted as form closing. */
- none = 0,
- /** Reorder non-form commands to the end, retaining order. */
- nonFormCommandsAfterForms
+ /** Perform no reordering, non-form commands will be interpreted as form closing. */
+ none = 0,
+ /** Reorder non-form commands to the end, retaining order. */
+ nonFormCommandsAfterForms
}
-// My kingdom for a real type system, or at least a way to autogenerate this in the
-// type system. I have given up on TypeScript actually helping me here.
-// TODO: Figure out a way to unit test this to make sure it's complete.
-/* eslint-disable prettier/prettier */
-/* eslint-disable @typescript-eslint/naming-convention */
-/** Enum of all possible commands that can be issued. */
-export enum CommandType {
- // Some printer commands can be command-language specific. This uses a different lookup table.
- CommandLanguageSpecificCommand = 'CommandLanguageSpecificCommand',
- // Users may supply printer commands. This uses a different lookup table.
- CommandCustomSpecificCommand = 'CommandCustomSpecificCommand',
- // Everything else is OOTB commands that should be implemented by internal implmentations.
- AddBoxCommand = 'AddBoxCommand',
- AddImageCommand = 'AddImageCommand',
- AddLineCommand = 'AddLineCommand',
- AutosenseLabelDimensionsCommand = 'AutosenseLabelDimensionsCommand',
- ClearImageBufferCommand = 'ClearImageBufferCommand',
- CutNowCommand = 'CutNowCommand',
- EnableFeedBackupCommand = 'EnableFeedBackupCommand',
- NewLabelCommand = 'NewLabelCommand',
- OffsetCommand = 'OffsetCommand',
- PrintCommand = 'PrintCommand',
- PrintConfigurationCommand = 'PrintConfigurationCommand',
- QueryConfigurationCommand = 'QueryConfigurationCommand',
- RawDocumentCommand = 'RawDocumentCommand',
- RebootPrinterCommand = 'RebootPrinterCommand',
- SaveCurrentConfigurationCommand = 'SaveCurrentConfigurationCommand',
- SetDarknessCommand = 'SetDarknessCommand',
- SetLabelDimensionsCommand = 'SetLabelDimensionsCommand',
- SetLabelHomeCommand = 'SetLabelHomeCommand',
- SetLabelPrintOriginOffsetCommand = 'SetLabelPrintOriginOffsetCommand',
- SetLabelToContinuousMediaCommand = 'SetLabelToContinuousMediaCommand',
- SetLabelToWebGapMediaCommand = 'SetLabelToWebGapMediaCommand',
- SetLabelToMarkMediaCommand = 'SetLabelToMarkMediaCommand',
- SetPrintDirectionCommand = 'SetPrintDirectionCommand',
- SetPrintSpeedCommand = 'SetPrintSpeedCommand',
- SuppressFeedBackupCommand = 'SuppressFeedBackupCommand',
-}
-/* eslint-enable @typescript-eslint/naming-convention */
-/* eslint-enable prettier/prettier */
+/** Union type of all possible commands that must be handled by command sets. */
+export type CommandType
+ // Users/PCLs may supply printer commands. This uses a different lookup table.
+ = "CustomCommand"
+ | "AddBoxCommand"
+ | "AddImageCommand"
+ | "AddLineCommand"
+ | "AutosenseLabelDimensionsCommand"
+ | "ClearImageBufferCommand"
+ | "CutNowCommand"
+ | "EnableFeedBackupCommand"
+ | "NewLabelCommand"
+ | "OffsetCommand"
+ | "PrintCommand"
+ | "PrintConfigurationCommand"
+ | "QueryConfigurationCommand"
+ | "RawDocumentCommand"
+ | "RebootPrinterCommand"
+ | "SaveCurrentConfigurationCommand"
+ | "SetDarknessCommand"
+ | "SetLabelDimensionsCommand"
+ | "SetLabelHomeCommand"
+ | "SetLabelPrintOriginOffsetCommand"
+ | "SetLabelToContinuousMediaCommand"
+ | "SetLabelToWebGapMediaCommand"
+ | "SetLabelToMarkMediaCommand"
+ | "SetPrintDirectionCommand"
+ | "SetPrintSpeedCommand"
+ |"SuppressFeedBackupCommand"
export class NewLabelCommand implements IPrinterCommand {
- get name(): string {
- return 'End previous label and begin a new label.';
- }
- get type() {
- return CommandType.NewLabelCommand;
- }
- toDisplay(): string {
- return this.name;
- }
+ get name() { return 'End previous label and begin a new label.'; }
+ get type(): CommandType { return 'NewLabelCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
}
export class PrintCommand implements IPrinterCommand {
- get name(): string {
- return 'Print label';
- }
- get type() {
- return CommandType.PrintCommand;
- }
- toDisplay(): string {
- return `Print ${this.count} copies of label`;
- }
-
- constructor(labelCount = 1, additionalDuplicateOfEach = 1) {
- // TODO: If someone complains that this is lower than what ZPL allows
- // figure out a way to support the 99,999,999 supported.
- // Who needs to print > 65 thousand labels at once??? I want to know.
- this.count = labelCount <= 0 || labelCount > 65535 ? 0 : labelCount;
- this.additionalDuplicateOfEach =
- additionalDuplicateOfEach <= 0 || additionalDuplicateOfEach > 65535
- ? 0
- : additionalDuplicateOfEach;
- }
-
- count: number;
- additionalDuplicateOfEach: number;
-
- printerEffectFlags = PrinterCommandEffectFlags.feedsLabel;
+ get name() { return 'Print label'; }
+ get type(): CommandType { return 'PrintCommand'; }
+ toDisplay(): string {
+ return `Print ${this.count} copies of label`;
+ }
+
+ constructor(labelCount = 1, additionalDuplicateOfEach = 1) {
+ // TODO: If someone complains that this is lower than what ZPL allows
+ // figure out a way to support the 99,999,999 supported.
+ // Who needs to print > 65 thousand labels at once??? I want to know.
+ this.count = labelCount <= 0 || labelCount > 65535 ? 0 : labelCount;
+ this.additionalDuplicateOfEach =
+ additionalDuplicateOfEach <= 0 || additionalDuplicateOfEach > 65535
+ ? 0
+ : additionalDuplicateOfEach;
+ }
+
+ count: number;
+ additionalDuplicateOfEach: number;
+
+ printerEffectFlags = PrinterCommandEffectFlags.feedsLabel;
}
export class CutNowCommand implements IPrinterCommand {
- get name(): string {
- return 'Cycle the media cutter now';
- }
- get type() {
- return CommandType.CutNowCommand;
- }
- toDisplay(): string {
- return this.name;
- }
+ get name() { return 'Cycle the media cutter now'; }
+ get type(): CommandType { return 'CutNowCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
- printerEffectFlags = PrinterCommandEffectFlags.actuatesCutter;
+ printerEffectFlags = PrinterCommandEffectFlags.actuatesCutter;
}
export class SuppressFeedBackupCommand implements IPrinterCommand {
- get name(): string {
- return 'Disable feed backup after printing label (be sure to re-enable!)';
- }
- get type() {
- return CommandType.SuppressFeedBackupCommand;
- }
- toDisplay(): string {
- return this.name;
- }
+ get name() { return 'Disable feed backup after printing label (be sure to re-enable!)'; }
+ get type(): CommandType { return 'SuppressFeedBackupCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
}
export class EnableFeedBackupCommand implements IPrinterCommand {
- get name(): string {
- return 'Enable feed backup after printing label.';
- }
- get type() {
- return CommandType.EnableFeedBackupCommand;
- }
- toDisplay(): string {
- return this.name;
- }
+ get name() { return 'Enable feed backup after printing label.'; }
+ get type(): CommandType { return 'EnableFeedBackupCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
}
/** A command to clear the image buffer. */
export class ClearImageBufferCommand implements IPrinterCommand {
- get name(): string {
- return 'Clear image buffer';
- }
- get type() {
- return CommandType.ClearImageBufferCommand;
- }
- toDisplay(): string {
- return this.name;
- }
+ get name() { return 'Clear image buffer'; }
+ get type(): CommandType { return 'ClearImageBufferCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
}
/** A command to have the printer send its configuration back over serial. */
export class QueryConfigurationCommand implements IPrinterCommand {
- get name(): string {
- return 'Query for printer config';
- }
- get type() {
- return CommandType.QueryConfigurationCommand;
- }
- toDisplay(): string {
- return this.name;
- }
+ get name() { return 'Query for printer config'; }
+ get type(): CommandType { return 'QueryConfigurationCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
}
/** A command to have the printer print its configuration labels. */
export class PrintConfigurationCommand implements IPrinterCommand {
- get name(): string {
- return "Print printer's config onto labels";
- }
- get type() {
- return CommandType.PrintConfigurationCommand;
- }
- toDisplay(): string {
- return this.name;
- }
- printerEffectFlags = PrinterCommandEffectFlags.feedsLabel;
+ get name() { return "Print printer's config onto labels"; }
+ get type(): CommandType { return 'PrintConfigurationCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.feedsLabel;
}
/** A command to store the current configuration as the stored configuration. */
export class SaveCurrentConfigurationCommand implements IPrinterCommand {
- get name(): string {
- return 'Store the current configuration as the saved configuration.';
- }
- get type(): CommandType {
- return CommandType.SaveCurrentConfigurationCommand;
- }
- toDisplay(): string {
- return this.name;
- }
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+ get name() { return 'Store the current configuration as the saved configuration.'; }
+ get type(): CommandType { return 'SaveCurrentConfigurationCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
}
/** A command to set the darkness the printer prints at. */
export class SetDarknessCommand implements IPrinterCommand {
- get name(): string {
- return 'Set darkness';
- }
- get type() {
- return CommandType.SetDarknessCommand;
- }
- toDisplay(): string {
- return `Set darkness to ${this.darknessPercent}%`;
- }
-
- constructor(darknessPercent: Options.DarknessPercent, darknessMax: number) {
- this.darknessPercent = darknessPercent;
- this.darknessMax = darknessMax;
- this.darknessSetting = Math.ceil((darknessPercent * darknessMax) / 100);
- }
-
- darknessPercent: Options.DarknessPercent;
- darknessMax: number;
- darknessSetting: number;
-
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+ get name() { return 'Set darkness'; }
+ get type(): CommandType { return 'SetDarknessCommand'; }
+ toDisplay(): string {
+ return `Set darkness to ${this.darknessPercent}%`;
+ }
+
+ constructor(
+ public readonly darknessPercent: Options.DarknessPercent,
+ public readonly darknessMax: number
+ ) {}
+
+ get darknessSetting() {
+ return Math.ceil((this.darknessPercent * this.darknessMax) / 100);
+ }
+
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
}
/** A command to set the direction a label prints, either upside down or not. */
export class SetPrintDirectionCommand implements IPrinterCommand {
- get name(): string {
- return 'Set print direction';
- }
- get type() {
- return CommandType.SetPrintDirectionCommand;
- }
- toDisplay(): string {
- return `Print labels ${this.upsideDown ? 'upside-down' : 'right-side up'}`;
- }
+ get name() { return 'Set print direction'; }
+ get type(): CommandType { return 'SetPrintDirectionCommand'; }
+ toDisplay(): string {
+ return `Print labels ${this.upsideDown ? 'upside-down' : 'right-side up'}`;
+ }
- constructor(upsideDown: boolean) {
- this.upsideDown = upsideDown;
- }
+ constructor(upsideDown: boolean) {
+ this.upsideDown = upsideDown;
+ }
- /** Whether to print labels upside-down. */
- upsideDown: boolean;
+ /** Whether to print labels upside-down. */
+ upsideDown: boolean;
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
}
/** A command to set the print speed a printer prints at. Support varies per printer. */
export class SetPrintSpeedCommand implements IPrinterCommand {
- get name(): string {
- return 'Set print speed';
- }
- get type() {
- return CommandType.SetPrintSpeedCommand;
- }
- toDisplay(): string {
- return `Set print speed to ${Options.PrintSpeed[this.speed]} (inches per second).`;
- }
-
- constructor(
- speed: Options.PrintSpeed,
- speedVal: number,
- mediaSlewSpeed: Options.PrintSpeed,
- mediaSpeedVal: number
- ) {
- this.speed = speed;
- this.speedVal = speedVal;
- this.mediaSlewSpeed = mediaSlewSpeed;
- this.mediaSpeedVal = mediaSpeedVal;
- }
-
- speed: Options.PrintSpeed;
- speedVal: number;
- mediaSlewSpeed: Options.PrintSpeed;
- mediaSpeedVal: number;
-
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+ get name() { return 'Set print speed'; }
+ get type(): CommandType { return 'SetPrintSpeedCommand'; }
+ toDisplay(): string {
+ return `Set print speed to ${Options.PrintSpeed[this.speed]} (inches per second).`;
+ }
+
+ constructor(
+ public readonly speed: Options.PrintSpeed,
+ public readonly speedVal: number,
+ public readonly mediaSlewSpeed: Options.PrintSpeed,
+ public readonly mediaSpeedVal: number
+ ) { }
+
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
}
/** A command to set the label dimensions of this label. */
export class SetLabelDimensionsCommand implements IPrinterCommand {
- get name(): string {
- return 'Set label dimensions';
- }
- get type() {
- return CommandType.SetLabelDimensionsCommand;
- }
- toDisplay(): string {
- let str = `Set label size to ${this.widthInDots} wide`;
- if (this.heightInDots) {
- str += ` x ${this.heightInDots} high`;
- }
- if (this.gapLengthInDots) {
- str += ` with a gap length of ${this.gapLengthInDots}`;
- }
- str += ' (in dots).';
- return str;
- }
-
- get setsHeight() {
- return this.heightInDots != null && this.gapLengthInDots != null;
- }
-
- // TODO: Black line mode for EPL?
- constructor(widthInDots: number, heightInDots?: number, gapLengthInDots?: number) {
- this.widthInDots = widthInDots;
- this.heightInDots = heightInDots;
- this.gapLengthInDots = gapLengthInDots;
- }
-
- widthInDots: number;
- heightInDots?: number;
- gapLengthInDots?: number;
-
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+ get name() { return 'Set label dimensions'; }
+ get type(): CommandType { return 'SetLabelDimensionsCommand'; }
+ toDisplay(): string {
+ let str = `Set label size to ${this.widthInDots} wide`;
+ if (this.heightInDots) {
+ str += ` x ${this.heightInDots} high`;
+ }
+ if (this.gapLengthInDots) {
+ str += ` with a gap length of ${this.gapLengthInDots}`;
+ }
+ str += ' (in dots).';
+ return str;
+ }
+
+ get setsHeight() {
+ return this.heightInDots !== undefined && this.gapLengthInDots !== undefined;
+ }
+
+ // TODO: Black line mode for EPL?
+ constructor(
+ public readonly widthInDots: number,
+ public readonly heightInDots?: number,
+ public readonly gapLengthInDots?: number) { }
+
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
}
export class SetLabelHomeCommand implements IPrinterCommand {
- get name(): string {
- return 'Sets the label home (origin) offset';
- }
- get type(): CommandType {
- return CommandType.SetLabelHomeCommand;
- }
- toDisplay(): string {
- return `Set the label home (origin) to ${this.xOffset},${this.yOffset} from the top-left.`;
- }
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
-
- constructor(public xOffset: number, public yOffset: number) {}
+ get name() { return 'Sets the label home (origin) offset'; }
+ get type(): CommandType { return 'SetLabelHomeCommand'; }
+ toDisplay(): string {
+ return `Set the label home (origin) to ${this.xOffset},${this.yOffset} from the top-left.`;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+
+ constructor(public xOffset: number, public yOffset: number) { }
}
/** Command class to set the print offset from the top-left of the label. */
export class SetLabelPrintOriginOffsetCommand implements IPrinterCommand {
- get name(): string {
- return 'Sets the print offset from the top left corner.';
- }
- get type(): CommandType {
- return CommandType.SetLabelPrintOriginOffsetCommand;
- }
- toDisplay(): string {
- return `Sets the print offset to ${this.xOffset} in and ${this.yOffset} down from the top-left.`;
- }
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
-
- constructor(public xOffset: number, public yOffset: number) {}
+ get name() { return 'Sets the print offset from the top left corner.'; }
+ get type(): CommandType { return 'SetLabelPrintOriginOffsetCommand'; }
+ toDisplay(): string {
+ return `Sets the print offset to ${this.xOffset} in and ${this.yOffset} down from the top-left.`;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+
+ constructor(public xOffset: number, public yOffset: number) { }
}
/** A command class to set the media handling mode to continuous media. */
export class SetLabelToContinuousMediaCommand implements IPrinterCommand {
- get name(): string {
- return 'Sets the media handling mode to continuous media.';
- }
- get type(): CommandType {
- return CommandType.SetLabelToContinuousMediaCommand;
- }
- toDisplay(): string {
- return this.name;
- }
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
-
- constructor(public labelLengthInDots: number, public labelGapInDots = 0) {}
+ get name() { return 'Sets the media handling mode to continuous media.'; }
+ get type(): CommandType { return 'SetLabelToContinuousMediaCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+
+ constructor(public labelLengthInDots: number, public labelGapInDots = 0) { }
}
/** A command class to set the media handling mode to web gap detection. */
export class SetLabelToWebGapMediaCommand implements IPrinterCommand {
- get name(): string {
- return 'Sets the media handling mode to web gap detection.';
- }
- get type(): CommandType {
- return CommandType.SetLabelToWebGapMediaCommand;
- }
- toDisplay(): string {
- return this.name;
- }
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
-
- constructor(public labelLengthInDots: number, public labelGapInDots: number) {}
+ get name() { return 'Sets the media handling mode to web gap detection.'; }
+ get type(): CommandType { return 'SetLabelToWebGapMediaCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+
+ constructor(public labelLengthInDots: number, public labelGapInDots: number) { }
}
/** A command class to set the media handling mode to black mark detection. */
export class SetLabelToMarkMediaCommand implements IPrinterCommand {
- get name(): string {
- return 'Sets the media handling mode to black mark detection.';
- }
- get type(): CommandType {
- return CommandType.SetLabelToMarkMediaCommand;
- }
- toDisplay(): string {
- return this.name;
- }
- printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
-
- constructor(
- public labelLengthInDots: number,
- public blackLineThicknessInDots: number,
- public blackLineOffset: number
- ) {}
+ get name() { return 'Sets the media handling mode to black mark detection.'; }
+ get type(): CommandType { return 'SetLabelToMarkMediaCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.altersPrinterConfig;
+
+ constructor(
+ public labelLengthInDots: number,
+ public blackLineThicknessInDots: number,
+ public blackLineOffset: number
+ ) { }
}
/** Command class to cause the printer to auto-sense the media length. */
export class AutosenseLabelDimensionsCommand implements IPrinterCommand {
- get name(): string {
- return 'Auto-sense the label length by feeding several labels.';
- }
- get type() {
- return CommandType.AutosenseLabelDimensionsCommand;
- }
- toDisplay(): string {
- return this.name;
- }
-
- printerEffectFlags =
- PrinterCommandEffectFlags.altersPrinterConfig | PrinterCommandEffectFlags.feedsLabel;
+ get name() { return 'Auto-sense the label length by feeding several labels.'; }
+ get type(): CommandType { return 'AutosenseLabelDimensionsCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+
+ printerEffectFlags =
+ PrinterCommandEffectFlags.altersPrinterConfig | PrinterCommandEffectFlags.feedsLabel;
}
/** Command class to modify an offset. */
export class OffsetCommand implements IPrinterCommand {
- get name(): string {
- return 'Modify offset';
- }
- get type() {
- return CommandType.OffsetCommand;
- }
- toDisplay(): string {
- let str = `Set offset to ${this.horizontal} from the left`;
- if (this.vertical) {
- str += ` and ${this.vertical} from the top`;
- }
- str += this.absolute ? `of the label.` : ` of the current offset.`;
- return str;
- }
-
- constructor(horizontal: number, vertical?: number, absolute = false) {
- this.horizontal = Math.floor(horizontal);
- this.vertical = Math.floor(vertical);
- this.absolute = absolute;
- }
-
- horizontal: number;
- vertical?: number;
- absolute = false;
+ get name() { return 'Modify offset'; }
+ get type(): CommandType { return 'OffsetCommand'; }
+ toDisplay(): string {
+ let str = `Set offset to ${this.horizontal} from the left`;
+ if (this.vertical) {
+ str += ` and ${this.vertical} from the top`;
+ }
+ str += this.absolute ? `of the label.` : ` of the current offset.`;
+ return str;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
+
+ constructor(horizontal: number, vertical?: number, absolute = false) {
+ this.horizontal = Math.floor(horizontal);
+ this.vertical = vertical !== undefined ? Math.floor(vertical) : undefined;
+ this.absolute = absolute;
+ }
+
+ horizontal: number;
+ vertical?: number;
+ absolute = false;
}
/** Command class to force a printer to reset. */
export class RebootPrinterCommand implements IPrinterCommand {
- get name(): string {
- return 'Simulate a power-cycle for the printer. This should be the final command.';
- }
- get type() {
- return CommandType.RebootPrinterCommand;
- }
- toDisplay(): string {
- return this.name;
- }
- printerEffectFlags = PrinterCommandEffectFlags.lossOfConnection;
+ get name() { return 'Simulate a power-cycle for the printer. This should be the final command.'; }
+ get type(): CommandType { return 'RebootPrinterCommand'; }
+ toDisplay(): string {
+ return this.name;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.lossOfConnection;
}
/** Command class to draw an image into the image buffer for immediate print. */
export class AddImageCommand implements IPrinterCommand {
- get name(): string {
- return 'Add image to label';
- }
- get type() {
- return CommandType.AddImageCommand;
- }
- toDisplay(): string {
- if (!this.bitmap) {
- return 'Adds a blank image';
- }
- return `Adds a ${this.bitmap.width} wide x ${this.bitmap.height} high image.`;
- }
-
- constructor(public bitmap: BitmapGRF, public imageConversionOptions: ImageConversionOptions) {}
+ get name() { return 'Add image to label'; }
+ get type(): CommandType { return 'AddImageCommand'; }
+ toDisplay(): string {
+ if (!this.bitmap) {
+ return 'Adds a blank image';
+ }
+ return `Adds a ${this.bitmap.width} wide x ${this.bitmap.height} high image.`;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
+
+ constructor(
+ public bitmap: BitmapGRF,
+ public imageConversionOptions: ImageConversionOptions
+ ) { }
}
/** Command class to draw a straight line. */
export class AddLineCommand implements IPrinterCommand {
- get name(): string {
- return 'Add perpendicular line to label';
- }
- get type() {
- return CommandType.AddLineCommand;
- }
- toDisplay(): string {
- // eslint-disable-next-line prettier/prettier
- return `Add a ${DrawColor[this.color]} line ${this.lengthInDots} wide by ${this.heightInDots} high.`;
- }
-
- constructor(lengthInDots: number, heightInDots: number, color: DrawColor) {
- this.lengthInDots = lengthInDots;
- this.heightInDots = heightInDots;
- this.color = color;
- }
-
- lengthInDots: number;
- heightInDots: number;
- color: DrawColor;
+ get name() { return 'Add perpendicular line to label'; }
+ get type(): CommandType { return 'AddLineCommand'; }
+ toDisplay(): string {
+ return `Add a ${DrawColor[this.color]} line ${this.lengthInDots} wide by ${this.heightInDots} high.`;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
+
+ constructor(
+ public readonly lengthInDots: number,
+ public readonly heightInDots: number,
+ public readonly color: DrawColor
+ ) { }
}
/** Command to draw a box on a label */
export class AddBoxCommand implements IPrinterCommand {
- get name(): string {
- return 'Add a box to label';
- }
- get type() {
- return CommandType.AddBoxCommand;
- }
- toDisplay(): string {
- return `Add a box ${this.lengthInDots} wide by ${this.heightInDots} high.`;
- }
-
- constructor(lengthInDots: number, heightInDots: number, thickness: number) {
- this.lengthInDots = lengthInDots;
- this.heightInDots = heightInDots;
- this.thickness = thickness;
- }
-
- lengthInDots: number;
- heightInDots: number;
- thickness: number;
+ get name() { return 'Add a box to label'; }
+ get type(): CommandType { return 'AddBoxCommand'; }
+ toDisplay(): string {
+ return `Add a box ${this.lengthInDots} wide by ${this.heightInDots} high.`;
+ }
+ printerEffectFlags = PrinterCommandEffectFlags.none;
+
+ constructor(
+ public readonly lengthInDots: number,
+ public readonly heightInDots: number,
+ public readonly thickness: number
+ ) { }
}
export class RawDocumentCommand implements IPrinterCommand {
- get name(): string {
- return 'Sends a raw set of commands directly to the printer unmodified.';
- }
- get type(): CommandType {
- return CommandType.RawDocumentCommand;
- }
- toDisplay(): string {
- throw new Error('Method not implemented.');
- }
- printerEffectFlags = PrinterCommandEffectFlags.unknownEffects;
-
- constructor(
- public rawDocument: string,
- printerEffectFlags = PrinterCommandEffectFlags.unknownEffects
- ) {
- this.printerEffectFlags = printerEffectFlags;
- }
+ get name() { return 'Sends a raw set of commands directly to the printer unmodified.'; }
+ get type(): CommandType { return 'RawDocumentCommand'; }
+ toDisplay(): string { return this.name; }
+
+ constructor(
+ public readonly rawDocument: string,
+ public readonly printerEffectFlags = PrinterCommandEffectFlags.unknownEffects
+ ) { }
}
diff --git a/src/Documents/ConfigDocument.ts b/src/Documents/ConfigDocument.ts
index 6dc5d37..5981290 100644
--- a/src/Documents/ConfigDocument.ts
+++ b/src/Documents/ConfigDocument.ts
@@ -1,282 +1,284 @@
import * as Commands from './Commands.js';
-import { IDocument, DocumentBuilder } from './Document.js';
+import { type IDocument, DocumentBuilder } from './Document.js';
import * as Options from '../Printers/Configuration/PrinterOptions.js';
import { WebZlpError } from '../WebZlpError.js';
/** A series of printer commands that results in configuration changes. */
export interface IConfigDocumentBuilder
- extends DocumentBuilder,
- IPrinterBasicCommandBuilder,
- IPrinterConfigBuilder,
- IPrinterLabelConfigBuilder {}
+ extends DocumentBuilder,
+ IPrinterBasicCommandBuilder,
+ IPrinterConfigBuilder,
+ IPrinterLabelConfigBuilder { }
/** Builder to generate a configuration to apply to a printer. */
export class ConfigDocumentBuilder
- extends DocumentBuilder
- implements IConfigDocumentBuilder
-{
- get commandReorderBehavior() {
- return Commands.CommandReorderBehavior.nonFormCommandsAfterForms;
+ extends DocumentBuilder
+ implements IConfigDocumentBuilder {
+ get commandReorderBehavior() {
+ return Commands.CommandReorderBehavior.nonFormCommandsAfterForms;
+ }
+
+ constructor(config?: Options.PrinterOptions) {
+ super(config ?? Options.PrinterOptions.invalid);
+ }
+
+ // The config document appends an additional command to the end of the document
+ // to commit the changes to stored memory. EPL does this automatically, ZPL does not
+ // so to bring them closer to parity this is automatically implied.
+ // TODO: Consider whether this should move to a ZPL extended command.
+ finalize() {
+ this.andThen(new Commands.SaveCurrentConfigurationCommand());
+ return super.finalize();
+ }
+
+ ///////////////////// GENERAL LABEL HANDLING
+
+ clearImageBuffer(): IConfigDocumentBuilder {
+ return this.andThen(new Commands.ClearImageBufferCommand());
+ }
+
+ rebootPrinter(): IDocument {
+ return this.andThen(new Commands.RebootPrinterCommand()).finalize();
+ }
+
+ ///////////////////// CONFIG READING
+
+ queryConfiguration(): IConfigDocumentBuilder {
+ return this.andThen(new Commands.QueryConfigurationCommand());
+ }
+
+ printConfiguration(): IDocument {
+ return this.andThen(new Commands.PrintConfigurationCommand()).finalize();
+ }
+
+ ///////////////////// ALTER PRINTER CONFIG
+
+ setDarknessConfig(darknessPercent: Options.DarknessPercent) {
+ return this.andThen(
+ new Commands.SetDarknessCommand(darknessPercent, this._config.model.maxDarkness)
+ );
+ }
+
+ setPrintDirection(upsideDown = false) {
+ return this.andThen(new Commands.SetPrintDirectionCommand(upsideDown));
+ }
+
+ setPrintSpeed(speed: Options.PrintSpeed, mediaSlewSpeed = Options.PrintSpeed.ipsAuto) {
+ if (!this._config.model.isSpeedValid(speed)) {
+ throw new UnsupportedPrinterConfigError(
+ 'setPrintSpeed',
+ `Print speed ${Options.PrintSpeed[speed]} is not valid for model ${this._config.model.model}`
+ );
}
+ const speedVal = this._config.model.getSpeedValue(speed);
- constructor(config: Options.PrinterOptions) {
- super(config);
+ // If the media slew speed is auto just copy the print speed.
+ if (mediaSlewSpeed === Options.PrintSpeed.ipsAuto) {
+ mediaSlewSpeed = speed;
}
-
- // The config document appends an additional command to the end of the document
- // to commit the changes to stored memory. EPL does this automatically, ZPL does not
- // so to bring them closer to parity this is automatically implied.
- // TODO: Consider whether this should move to a ZPL extended command.
- finalize() {
- this.andThen(new Commands.SaveCurrentConfigurationCommand());
- return super.finalize();
- }
-
- ///////////////////// GENERAL LABEL HANDLING
-
- clearImageBuffer(): IConfigDocumentBuilder {
- return this.andThen(new Commands.ClearImageBufferCommand());
- }
-
- rebootPrinter(): IDocument {
- return this.andThen(new Commands.RebootPrinterCommand()).finalize();
- }
-
- ///////////////////// CONFIG READING
-
- queryConfiguration(): IConfigDocumentBuilder {
- return this.andThen(new Commands.QueryConfigurationCommand());
- }
-
- printConfiguration(): IDocument {
- return this.andThen(new Commands.PrintConfigurationCommand()).finalize();
- }
-
- ///////////////////// ALTER PRINTER CONFIG
-
- setDarknessConfig(darknessPercent: Options.DarknessPercent) {
- return this.andThen(
- new Commands.SetDarknessCommand(darknessPercent, this._config.model.maxDarkness)
- );
- }
-
- setPrintDirection(upsideDown = false) {
- return this.andThen(new Commands.SetPrintDirectionCommand(upsideDown));
- }
-
- setPrintSpeed(speed: Options.PrintSpeed, mediaSlewSpeed = Options.PrintSpeed.ipsAuto) {
- if (!this._config.model.isSpeedValid(speed)) {
- throw new UnsupportedPrinterConfigError(
- 'setPrintSpeed',
- `Print speed ${Options.PrintSpeed[speed]} is not valid for model ${this._config.model.model}`
- );
- }
- if (mediaSlewSpeed && !this._config.model.isSpeedValid(mediaSlewSpeed)) {
- throw new UnsupportedPrinterConfigError(
- 'setPrintSpeed',
- `Media slew speed ${Options.PrintSpeed[speed]} is not valid for model ${this._config.model.model}`
- );
- }
-
- // If the media slew speed is auto just copy the print speed.
- if (mediaSlewSpeed === Options.PrintSpeed.ipsAuto) {
- mediaSlewSpeed = speed;
- }
- return this.andThen(
- new Commands.SetPrintSpeedCommand(
- speed,
- this._config.model.getSpeedValue(speed),
- mediaSlewSpeed,
- this._config.model.getSpeedValue(mediaSlewSpeed)
- )
- );
- }
-
- ///////////////////// ALTER LABEL CONFIG
-
- autosenseLabelLength() {
- return this.andThen(new Commands.AutosenseLabelDimensionsCommand()).finalize();
- }
-
- setLabelDimensions(widthInInches: number, heightInInches?: number, gapLengthInInches?: number) {
- const dpi = this._config.model.dpi;
- return this.setLabelDimensionsDots(
- widthInInches * dpi,
- heightInInches ? heightInInches * dpi : null,
- gapLengthInInches ? gapLengthInInches * dpi : null
- );
- }
-
- setLabelDimensionsDots(widthInDots: number, heightInDots?: number, gapLengthInDots?: number) {
- return this.andThen(
- new Commands.SetLabelDimensionsCommand(widthInDots, heightInDots, gapLengthInDots)
- );
- }
-
- setLabelHomeOffsetDots(horizontalOffsetInDots: number, verticalOffsetInDots: number) {
- return this.andThen(
- new Commands.SetLabelHomeCommand(horizontalOffsetInDots, verticalOffsetInDots)
- );
- }
-
- setLabelPrintOriginOffsetCommand(horizontalOffsetInDots: number, verticalOffsetInDots: number) {
- return this.andThen(
- new Commands.SetLabelPrintOriginOffsetCommand(
- horizontalOffsetInDots,
- verticalOffsetInDots
- )
- );
- }
-
- setLabelMediaToContinuous(labelHeightInInches: number): IConfigDocumentBuilder {
- const dpi = this._config.model.dpi;
- return this.andThen(
- new Commands.SetLabelToContinuousMediaCommand(dpi * labelHeightInInches)
- );
- }
-
- setLabelMediaToWebGapSense(
- labelHeightInInches: number,
- labelGapInInches: number
- ): IConfigDocumentBuilder {
- const dpi = this._config.model.dpi;
- return this.andThen(
- new Commands.SetLabelToWebGapMediaCommand(
- labelHeightInInches * dpi,
- labelGapInInches * dpi
- )
- );
- }
-
- setLabelMediaToMarkSense(
- labelLengthInInches: number,
- blackLineThicknessInInches: number,
- blackLineOffsetInInches: number
- ): IConfigDocumentBuilder {
- const dpi = this._config.model.dpi;
- return this.andThen(
- new Commands.SetLabelToMarkMediaCommand(
- labelLengthInInches * dpi,
- blackLineThicknessInInches * dpi,
- blackLineOffsetInInches * dpi
- )
- );
+ if (mediaSlewSpeed && !this._config.model.isSpeedValid(mediaSlewSpeed)) {
+ throw new UnsupportedPrinterConfigError(
+ 'setPrintSpeed',
+ `Media slew speed ${Options.PrintSpeed[speed]} is not valid for model ${this._config.model.model}`
+ );
}
+ const mediaSpeedVal = this._config.model.getSpeedValue(mediaSlewSpeed);
+
+ return this.andThen(
+ new Commands.SetPrintSpeedCommand(
+ speed,
+ speedVal,
+ mediaSlewSpeed,
+ mediaSpeedVal,
+ )
+ );
+ }
+
+ ///////////////////// ALTER LABEL CONFIG
+
+ autosenseLabelLength() {
+ return this.andThen(new Commands.AutosenseLabelDimensionsCommand()).finalize();
+ }
+
+ setLabelDimensions(widthInInches: number, heightInInches?: number, gapLengthInInches?: number) {
+ const dpi = this._config.model.dpi;
+ return this.setLabelDimensionsDots(
+ widthInInches * dpi,
+ heightInInches ? heightInInches * dpi : undefined,
+ gapLengthInInches ? gapLengthInInches * dpi : undefined
+ );
+ }
+
+ setLabelDimensionsDots(widthInDots: number, heightInDots?: number, gapLengthInDots?: number) {
+ return this.andThen(
+ new Commands.SetLabelDimensionsCommand(widthInDots, heightInDots, gapLengthInDots)
+ );
+ }
+
+ setLabelHomeOffsetDots(horizontalOffsetInDots: number, verticalOffsetInDots: number) {
+ return this.andThen(
+ new Commands.SetLabelHomeCommand(horizontalOffsetInDots, verticalOffsetInDots)
+ );
+ }
+
+ setLabelPrintOriginOffsetCommand(horizontalOffsetInDots: number, verticalOffsetInDots: number) {
+ return this.andThen(
+ new Commands.SetLabelPrintOriginOffsetCommand(
+ horizontalOffsetInDots,
+ verticalOffsetInDots
+ )
+ );
+ }
+
+ setLabelMediaToContinuous(labelHeightInInches: number): IConfigDocumentBuilder {
+ const dpi = this._config.model.dpi;
+ return this.andThen(
+ new Commands.SetLabelToContinuousMediaCommand(dpi * labelHeightInInches)
+ );
+ }
+
+ setLabelMediaToWebGapSense(
+ labelHeightInInches: number,
+ labelGapInInches: number
+ ): IConfigDocumentBuilder {
+ const dpi = this._config.model.dpi;
+ return this.andThen(
+ new Commands.SetLabelToWebGapMediaCommand(
+ labelHeightInInches * dpi,
+ labelGapInInches * dpi
+ )
+ );
+ }
+
+ setLabelMediaToMarkSense(
+ labelLengthInInches: number,
+ blackLineThicknessInInches: number,
+ blackLineOffsetInInches: number
+ ): IConfigDocumentBuilder {
+ const dpi = this._config.model.dpi;
+ return this.andThen(
+ new Commands.SetLabelToMarkMediaCommand(
+ labelLengthInInches * dpi,
+ blackLineThicknessInInches * dpi,
+ blackLineOffsetInInches * dpi
+ )
+ );
+ }
}
export interface IPrinterBasicCommandBuilder {
- /** Clear the image buffer and prepare for a new set of commands. */
- clearImageBuffer(): IConfigDocumentBuilder;
+ /** Clear the image buffer and prepare for a new set of commands. */
+ clearImageBuffer(): IConfigDocumentBuilder;
- /** Simulate turning the printer off and back on. Must be the final command. */
- rebootPrinter(): IDocument;
+ /** Simulate turning the printer off and back on. Must be the final command. */
+ rebootPrinter(): IDocument;
}
export interface IPrinterConfigBuilder {
- /** Query the printer for its config details. */
- queryConfiguration(): IConfigDocumentBuilder;
+ /** Query the printer for its config details. */
+ queryConfiguration(): IConfigDocumentBuilder;
- /** Print the configuration directly on labels. Must be final command. */
- printConfiguration(): IDocument;
+ /** Print the configuration directly on labels. Must be final command. */
+ printConfiguration(): IDocument;
}
export interface IPrinterLabelConfigBuilder {
- /** Set the darkness of the printer in the stored configuration. */
- setDarknessConfig(darknessPercent: Options.DarknessPercent): IConfigDocumentBuilder;
-
- /** Set the direction labels print out of the printer. */
- setPrintDirection(upsideDown?: boolean): IConfigDocumentBuilder;
-
- /** Set the speed at which the labels print. */
- setPrintSpeed(speed: Options.PrintSpeed): IConfigDocumentBuilder;
-
- /**
- * Set the size of the labels in the printer.
- *
- * Omit height and gap if an autosense was or will be run. Both must be provided
- * to set the length and gap manually, otherwise the length will be ignored. This
- * is usually only necessary when using continuous media, see the documentation.
- *
- * Note that different printers often have slightly different values, copying
- * values between printers may have unintended effects.
- */
- setLabelDimensions(
- widthInInches: number,
- heightInInches?: number,
- gapLengthInInches?: number
- ): IConfigDocumentBuilder;
-
- /**
- * Set the size of the labels in the printer, sized in dots. Dots are printer DPI specific.
- *
- * Omit height and gap if an autosense was or will be run. Both must be provided
- * to set the length and gap manually, otherwise the length will be ignored. This
- * is usually only necessary when using continuous media, see the documentation.
- *
- * Note that different printers often have slightly different values, copying
- * values between printers may have unintended effects.
- */
- setLabelDimensionsDots(
- widthInDots: number,
- heightInDots?: number,
- gapLengthInDots?: number
- ): IConfigDocumentBuilder;
-
- /**
- * Sets the temporary origin offset from the top-left of the label that all
- * other offsets are calculated from. Only applies to current label.
- *
- * Use this to fine-tune the alignment of your printer to your label stock.
- *
- * Avoid printing off the edges of a label, which can cause excessive head wear.
- */
- setLabelHomeOffsetDots(
- horizontalOffsetInDots: number,
- verticalOffsetInDots: number
- ): IConfigDocumentBuilder;
-
- /** Sets the retained origin offset from the top-left of the label that all
- * other offets are calculated from. Applies to all labels until a printer reset
- * or power cycle.
- *
- * May or may not be stored depending on printer firmware.
- *
- * Avoid printing off the edges of a label, which can cause excessive head wear.
- */
- setLabelPrintOriginOffsetCommand(
- horizontalOffsetInDots: number,
- verticalOffsetInDots: number
- ): IConfigDocumentBuilder;
-
- /** Run the autosense operation to get label length. Must be last command. */
- autosenseLabelLength(): IDocument;
-
- /** Sets the media type to continuous (gapless) media. */
- setLabelMediaToContinuous(labelLengthInDots: number): IConfigDocumentBuilder;
-
- /** Sets the media type to web gap sensing media. It's recommended to run autosense after this. */
- setLabelMediaToWebGapSense(
- labelLengthInDots: number,
- labelGapInDots: number
- ): IConfigDocumentBuilder;
-
- /** Sets the media type to mark sensing media. */
- setLabelMediaToMarkSense(
- labelLengthInDots: number,
- blackLineThicknessInDots: number,
- blackLineOffset: number
- ): IConfigDocumentBuilder;
+ /** Set the darkness of the printer in the stored configuration. */
+ setDarknessConfig(darknessPercent: Options.DarknessPercent): IConfigDocumentBuilder;
+
+ /** Set the direction labels print out of the printer. */
+ setPrintDirection(upsideDown?: boolean): IConfigDocumentBuilder;
+
+ /** Set the speed at which the labels print. */
+ setPrintSpeed(speed: Options.PrintSpeed): IConfigDocumentBuilder;
+
+ /**
+ * Set the size of the labels in the printer.
+ *
+ * Omit height and gap if an autosense was or will be run. Both must be provided
+ * to set the length and gap manually, otherwise the length will be ignored. This
+ * is usually only necessary when using continuous media, see the documentation.
+ *
+ * Note that different printers often have slightly different values, copying
+ * values between printers may have unintended effects.
+ */
+ setLabelDimensions(
+ widthInInches: number,
+ heightInInches?: number,
+ gapLengthInInches?: number
+ ): IConfigDocumentBuilder;
+
+ /**
+ * Set the size of the labels in the printer, sized in dots. Dots are printer DPI specific.
+ *
+ * Omit height and gap if an autosense was or will be run. Both must be provided
+ * to set the length and gap manually, otherwise the length will be ignored. This
+ * is usually only necessary when using continuous media, see the documentation.
+ *
+ * Note that different printers often have slightly different values, copying
+ * values between printers may have unintended effects.
+ */
+ setLabelDimensionsDots(
+ widthInDots: number,
+ heightInDots?: number,
+ gapLengthInDots?: number
+ ): IConfigDocumentBuilder;
+
+ /**
+ * Sets the temporary origin offset from the top-left of the label that all
+ * other offsets are calculated from. Only applies to current label.
+ *
+ * Use this to fine-tune the alignment of your printer to your label stock.
+ *
+ * Avoid printing off the edges of a label, which can cause excessive head wear.
+ */
+ setLabelHomeOffsetDots(
+ horizontalOffsetInDots: number,
+ verticalOffsetInDots: number
+ ): IConfigDocumentBuilder;
+
+ /** Sets the retained origin offset from the top-left of the label that all
+ * other offets are calculated from. Applies to all labels until a printer reset
+ * or power cycle.
+ *
+ * May or may not be stored depending on printer firmware.
+ *
+ * Avoid printing off the edges of a label, which can cause excessive head wear.
+ */
+ setLabelPrintOriginOffsetCommand(
+ horizontalOffsetInDots: number,
+ verticalOffsetInDots: number
+ ): IConfigDocumentBuilder;
+
+ /** Run the autosense operation to get label length. Must be last command. */
+ autosenseLabelLength(): IDocument;
+
+ /** Sets the media type to continuous (gapless) media. */
+ setLabelMediaToContinuous(labelLengthInDots: number): IConfigDocumentBuilder;
+
+ /** Sets the media type to web gap sensing media. It's recommended to run autosense after this. */
+ setLabelMediaToWebGapSense(
+ labelLengthInDots: number,
+ labelGapInDots: number
+ ): IConfigDocumentBuilder;
+
+ /** Sets the media type to mark sensing media. */
+ setLabelMediaToMarkSense(
+ labelLengthInDots: number,
+ blackLineThicknessInDots: number,
+ blackLineOffset: number
+ ): IConfigDocumentBuilder;
}
/** Error indicating setting a config value failed. */
export class UnsupportedPrinterConfigError extends WebZlpError {
- constructor(settingName: string, settingError: string) {
- super(`Error setting ${settingName}: ${settingError}`);
- this.name = this.constructor.name;
- this.settingName = settingName;
- this.settingError = settingError;
- }
-
- settingName: string;
- settingError: string;
+ constructor(settingName: string, settingError: string) {
+ super(`Error setting ${settingName}: ${settingError}`);
+ this.name = this.constructor.name;
+ this.settingName = settingName;
+ this.settingError = settingError;
+ }
+
+ settingName: string;
+ settingError: string;
}
diff --git a/src/Documents/Document.ts b/src/Documents/Document.ts
index e958f38..abcd146 100644
--- a/src/Documents/Document.ts
+++ b/src/Documents/Document.ts
@@ -3,80 +3,80 @@ import * as Options from '../Printers/Configuration/PrinterOptions.js';
/** A prepared document, ready to be compiled and sent. */
export interface IDocument {
- /** Gets the series of commands this document contains. */
- get commands(): ReadonlyArray;
+ /** Gets the series of commands this document contains. */
+ get commands(): ReadonlyArray;
- /** Gets the behavior allowed for reordering commands in this document. */
- get commandReorderBehavior(): Commands.CommandReorderBehavior;
+ /** Gets the behavior allowed for reordering commands in this document. */
+ get commandReorderBehavior(): Commands.CommandReorderBehavior;
- /** Return the list of commands that will be performed in human-readable format. */
- showCommands(): string;
+ /** Return the list of commands that will be performed in human-readable format. */
+ showCommands(): string;
}
export class Document implements IDocument {
- constructor(
- public readonly commands: ReadonlyArray,
- public readonly commandReorderBehavior = Commands.CommandReorderBehavior.none
- ) {}
+ constructor(
+ public readonly commands: ReadonlyArray,
+ public readonly commandReorderBehavior = Commands.CommandReorderBehavior.none
+ ) { }
- /** Display the commands that will be performed in a human-readable format. */
- public showCommands(): string {
- return this.commands.map((c) => c.toDisplay()).join('\n');
- }
+ /** Display the commands that will be performed in a human-readable format. */
+ public showCommands(): string {
+ return this.commands.map((c) => c.toDisplay()).join('\n');
+ }
}
/** A document of raw commands, ready to be sent to a printer. */
export class CompiledDocument {
- constructor(
- public readonly commandLanguage: Options.PrinterCommandLanguage,
- public readonly effectFlags: Commands.PrinterCommandEffectFlags,
- public readonly commandBuffer: Uint8Array
- ) {}
+ constructor(
+ public readonly commandLanguage: Options.PrinterCommandLanguage,
+ public readonly effectFlags: Commands.PrinterCommandEffectFlags,
+ public readonly commandBuffer: Uint8Array
+ ) { }
- /**
- * Gets the text view of the command buffer. Do not send this to the printer, the encoding
- * will break and commands will fail.
- */
- get commandBufferString(): string {
- return new TextDecoder('ascii').decode(this.commandBuffer);
- }
+ /**
+ * Gets the text view of the command buffer. Do not send this to the printer, the encoding
+ * will break and commands will fail.
+ */
+ get commandBufferString(): string {
+ return new TextDecoder('ascii').decode(this.commandBuffer);
+ }
}
/** A basic document builder, containing internal state to construct a document. */
export abstract class DocumentBuilder> {
- private _commands: Commands.IPrinterCommand[] = [];
- protected _config: Options.PrinterOptions;
+ private _commands: Commands.IPrinterCommand[] = [];
+ protected _config: Options.PrinterOptions;
- /** The reordering behavior for commands that should not be present within a document. */
- abstract get commandReorderBehavior(): Commands.CommandReorderBehavior;
+ /** The reordering behavior for commands that should not be present within a document. */
+ abstract get commandReorderBehavior(): Commands.CommandReorderBehavior;
- constructor(config: Options.PrinterOptions) {
- this._config = config;
- }
+ constructor(config: Options.PrinterOptions) {
+ this._config = config;
+ }
- /** Gets a read-only copy of the current label configuration. */
- get currentConfig() {
- return structuredClone(this._config);
- }
+ /** Gets a read-only copy of the current label configuration. */
+ get currentConfig() {
+ return structuredClone(this._config);
+ }
- /** Clear the commands in this document and reset it to the starting blank. */
- clear(): TBuilder {
- this._commands = [];
- return this as unknown as TBuilder;
- }
+ /** Clear the commands in this document and reset it to the starting blank. */
+ clear(): TBuilder {
+ this._commands = [];
+ return this as unknown as TBuilder;
+ }
- /** Return the list of commands that will be performed in human-readable format. */
- showCommands(): string {
- return this._commands.map((c) => c.toDisplay()).join('\n');
- }
+ /** Return the list of commands that will be performed in human-readable format. */
+ showCommands(): string {
+ return this._commands.map((c) => c.toDisplay()).join('\n');
+ }
- /** Return the final built document. */
- finalize(): Document {
- return new Document(this._commands, this.commandReorderBehavior);
- }
+ /** Return the final built document. */
+ finalize(): Document {
+ return new Document(this._commands, this.commandReorderBehavior);
+ }
- protected andThen(command: Commands.IPrinterCommand): TBuilder {
- this._commands.push(command);
- return this as unknown as TBuilder;
- }
+ protected andThen(...command: Commands.IPrinterCommand[]): TBuilder {
+ this._commands.push(...command);
+ return this as unknown as TBuilder;
+ }
}
diff --git a/src/Documents/LabelDocument.ts b/src/Documents/LabelDocument.ts
index f35dff7..b8a2525 100644
--- a/src/Documents/LabelDocument.ts
+++ b/src/Documents/LabelDocument.ts
@@ -1,215 +1,212 @@
import * as Commands from './Commands.js';
import { DocumentBuilder } from './Document.js';
import * as Options from '../Printers/Configuration/PrinterOptions.js';
-import { BitmapGRF, ImageConversionOptions } from './BitmapGRF.js';
+import { BitmapGRF, type ImageConversionOptions } from './BitmapGRF.js';
export interface ILabelDocumentBuilder
- extends DocumentBuilder,
- ILabelActionCommandBuilder,
- ILabelPositionCommandBuilder,
- ILabelContentCommandBuilder {}
+ extends DocumentBuilder,
+ ILabelActionCommandBuilder,
+ ILabelPositionCommandBuilder,
+ ILabelContentCommandBuilder { }
export class LabelDocumentBuilder
- extends DocumentBuilder
- implements ILabelDocumentBuilder
-{
+ extends DocumentBuilder
+ implements ILabelDocumentBuilder {
+
+ get commandReorderBehavior(): Commands.CommandReorderBehavior {
+ return Commands.CommandReorderBehavior.none;
+ }
+
+ constructor(
+ config?: Options.PrinterOptions,
// TOOD: Implement other document types, such as stored forms, with type safety
// so that only certain commands can be used on them.
// Maybe different types??
- private _docType: LabelDocumentType = LabelDocumentType.instanceForm;
-
- get commandReorderBehavior(): Commands.CommandReorderBehavior {
- return Commands.CommandReorderBehavior.none;
- }
-
- constructor(
- config: Options.PrinterOptions,
- docType: LabelDocumentType = LabelDocumentType.instanceForm
- ) {
- super(config);
- this._docType = docType;
- }
-
- ///////////////////// GENERAL LABEL HANDLING
-
- clearImageBuffer(): ILabelDocumentBuilder {
- return this.andThen(new Commands.ClearImageBufferCommand());
- }
-
- addPrintCmd(count?: number, additionalDuplicateOfEach?: number): ILabelDocumentBuilder {
- return this.andThen(new Commands.PrintCommand(count ?? 1, additionalDuplicateOfEach ?? 0));
- }
-
- addCutNowCommand(): ILabelDocumentBuilder {
- return this.andThen(new Commands.CutNowCommand());
- }
-
- startNewLabel(): ILabelDocumentBuilder {
- return this.andThen(new Commands.NewLabelCommand());
- }
-
- suppressFeedBackupForLabel(): ILabelDocumentBuilder {
- return this.andThen(new Commands.SuppressFeedBackupCommand());
- }
-
- reenableFeedBackup(): ILabelDocumentBuilder {
- return this.andThen(new Commands.EnableFeedBackupCommand());
- }
-
- ///////////////////// OFFSET AND SPACING
-
- setOffset(horizontal: number, vertical?: number): ILabelDocumentBuilder {
- return this.andThen(new Commands.OffsetCommand(horizontal, vertical, true));
- }
-
- setLabelHomeOffsetDots(horizontalOffsetInDots: number, verticalOffsetInDots: number) {
- return this.andThen(
- new Commands.SetLabelHomeCommand(horizontalOffsetInDots, verticalOffsetInDots)
- );
- }
-
- addOffset(horizontal: number, vertical?: number): ILabelDocumentBuilder {
- return this.andThen(new Commands.OffsetCommand(horizontal, vertical));
- }
-
- resetOffset(): ILabelDocumentBuilder {
- return this.andThen(new Commands.OffsetCommand(0, 0, true));
- }
-
- ///////////////////// LABEL IMAGE CONTENTS
-
- addImageFromImageData(
- imageData: ImageData,
- imageConversionOptions: ImageConversionOptions = {}
- ): ILabelDocumentBuilder {
- return this.andThen(
- new Commands.AddImageCommand(
- BitmapGRF.fromCanvasImageData(imageData),
- imageConversionOptions
- )
- );
- }
-
- addImageFromGRF(
- image: BitmapGRF,
- imageConversionOptions: ImageConversionOptions = {}
- ): ILabelDocumentBuilder {
- return this.andThen(new Commands.AddImageCommand(image, imageConversionOptions));
- }
-
- async addImageFromSVG(
- svg: string,
- widthInDots: number,
- heightInDots: number,
- imageConversionOptions: ImageConversionOptions = {}
- ): Promise {
- const img = await BitmapGRF.fromSVG(svg, widthInDots, heightInDots, imageConversionOptions);
- const result = this.addImageFromGRF(img, imageConversionOptions);
- return result;
- }
-
- addLine(lengthInDots: number, heightInDots: number, color = Commands.DrawColor.black) {
- return this.andThen(new Commands.AddLineCommand(lengthInDots, heightInDots, color));
- }
-
- addBox(
- lengthInDots: number,
- heightInDots: number,
- thicknessInDots: number
- ): ILabelDocumentBuilder {
- return this.andThen(
- new Commands.AddBoxCommand(lengthInDots, heightInDots, thicknessInDots)
- );
- }
+ public readonly docType: LabelDocumentType = LabelDocumentType.instanceForm
+ ) {
+ super(config ?? Options.PrinterOptions.invalid);
+ }
+
+ ///////////////////// GENERAL LABEL HANDLING
+
+ clearImageBuffer(): ILabelDocumentBuilder {
+ return this.andThen(new Commands.ClearImageBufferCommand());
+ }
+
+ addPrintCmd(count?: number, additionalDuplicateOfEach?: number): ILabelDocumentBuilder {
+ return this.andThen(new Commands.PrintCommand(count ?? 1, additionalDuplicateOfEach ?? 0));
+ }
+
+ addCutNowCommand(): ILabelDocumentBuilder {
+ return this.andThen(new Commands.CutNowCommand());
+ }
+
+ startNewLabel(): ILabelDocumentBuilder {
+ return this.andThen(new Commands.NewLabelCommand());
+ }
+
+ suppressFeedBackupForLabel(): ILabelDocumentBuilder {
+ return this.andThen(new Commands.SuppressFeedBackupCommand());
+ }
+
+ reenableFeedBackup(): ILabelDocumentBuilder {
+ return this.andThen(new Commands.EnableFeedBackupCommand());
+ }
+
+ ///////////////////// OFFSET AND SPACING
+
+ setOffset(horizontal: number, vertical?: number): ILabelDocumentBuilder {
+ return this.andThen(new Commands.OffsetCommand(horizontal, vertical, true));
+ }
+
+ setLabelHomeOffsetDots(horizontalOffsetInDots: number, verticalOffsetInDots: number) {
+ return this.andThen(
+ new Commands.SetLabelHomeCommand(horizontalOffsetInDots, verticalOffsetInDots)
+ );
+ }
+
+ addOffset(horizontal: number, vertical?: number): ILabelDocumentBuilder {
+ return this.andThen(new Commands.OffsetCommand(horizontal, vertical));
+ }
+
+ resetOffset(): ILabelDocumentBuilder {
+ return this.andThen(new Commands.OffsetCommand(0, 0, true));
+ }
+
+ ///////////////////// LABEL IMAGE CONTENTS
+
+ addImageFromImageData(
+ imageData: ImageData,
+ imageConversionOptions: ImageConversionOptions = {}
+ ): ILabelDocumentBuilder {
+ return this.andThen(
+ new Commands.AddImageCommand(
+ BitmapGRF.fromCanvasImageData(imageData),
+ imageConversionOptions
+ )
+ );
+ }
+
+ addImageFromGRF(
+ image: BitmapGRF,
+ imageConversionOptions: ImageConversionOptions = {}
+ ): ILabelDocumentBuilder {
+ return this.andThen(new Commands.AddImageCommand(image, imageConversionOptions));
+ }
+
+ async addImageFromSVG(
+ svg: string,
+ widthInDots: number,
+ heightInDots: number,
+ imageConversionOptions: ImageConversionOptions = {}
+ ): Promise {
+ const img = await BitmapGRF.fromSVG(svg, widthInDots, heightInDots, imageConversionOptions);
+ const result = this.addImageFromGRF(img, imageConversionOptions);
+ return result;
+ }
+
+ addLine(lengthInDots: number, heightInDots: number, color = Commands.DrawColor.black) {
+ return this.andThen(new Commands.AddLineCommand(lengthInDots, heightInDots, color));
+ }
+
+ addBox(
+ lengthInDots: number,
+ heightInDots: number,
+ thicknessInDots: number
+ ): ILabelDocumentBuilder {
+ return this.andThen(
+ new Commands.AddBoxCommand(lengthInDots, heightInDots, thicknessInDots)
+ );
+ }
}
/** Types of label documents to send to printers. See the docs. */
export enum LabelDocumentType {
- /** A form that is only used for one set of commands, then discarded. */
- instanceForm,
- /** A form that is stored in the printer to be re-used. */
- storedForm,
- /** A form that is rendered as an image before being sent to the printer. */
- imageForm
+ /** A form that is only used for one set of commands, then discarded. */
+ instanceForm,
+ /** A form that is stored in the printer to be re-used. */
+ storedForm,
+ /** A form that is rendered as an image before being sent to the printer. */
+ imageForm
}
export interface ILabelActionCommandBuilder {
- /** Add a commant to print a number of the preceding label instructions. Defaults to 1 */
- addPrintCmd(count?: number, additionalDuplicateOfEach?: number): ILabelDocumentBuilder;
+ /** Add a commant to print a number of the preceding label instructions. Defaults to 1 */
+ addPrintCmd(count?: number, additionalDuplicateOfEach?: number): ILabelDocumentBuilder;
- /** Clear the image buffer to prepare for a new label. Usually only at the start of a label. */
- clearImageBuffer(): ILabelDocumentBuilder;
+ /** Clear the image buffer to prepare for a new label. Usually only at the start of a label. */
+ clearImageBuffer(): ILabelDocumentBuilder;
- /** Add a command to cut a label, usually at the end of a label and right before the print command. */
- addCutNowCommand(): ILabelDocumentBuilder;
+ /** Add a command to cut a label, usually at the end of a label and right before the print command. */
+ addCutNowCommand(): ILabelDocumentBuilder;
- /** Begin a new label to be sent as a single batch. */
- startNewLabel(): ILabelDocumentBuilder;
+ /** Begin a new label to be sent as a single batch. */
+ startNewLabel(): ILabelDocumentBuilder;
- /** Disable feed backup for this label. Be sure to re-enable at the end of the batch. */
- suppressFeedBackupForLabel(): ILabelDocumentBuilder;
+ /** Disable feed backup for this label. Be sure to re-enable at the end of the batch. */
+ suppressFeedBackupForLabel(): ILabelDocumentBuilder;
- /** Re-enable feed backup for this and future labels. */
- reenableFeedBackup(): ILabelDocumentBuilder;
+ /** Re-enable feed backup for this and future labels. */
+ reenableFeedBackup(): ILabelDocumentBuilder;
}
export interface ILabelPositionCommandBuilder {
- /** Set the aboslute offset from the top left position of the label.
- *
- * Avoid printing off the edges of a label, which can cause excessive head wear.
- */
- setOffset(horizontal: number, vertical?: number): ILabelDocumentBuilder;
-
- /**
- * Sets the temporary origin offset from the top-left of the label that all
- * other offsets are calculated from. Only applies to current label.
- *
- * Avoid printing off the edges of a label, which can cause excessive head wear.
- */
- setLabelHomeOffsetDots(
- horizontalOffsetInDots: number,
- verticalOffsetInDots: number
- ): ILabelDocumentBuilder;
-
- /** Add a relative offset to the current offset from the top left position of the label.
- *
- * Avoid printing off the edges of a label, which can cause excessive head wear.
- */
- addOffset(horizontal: number, vertical?: number): ILabelDocumentBuilder;
-
- /** Resets the offset back to origin (top left of label) */
- resetOffset(): ILabelDocumentBuilder;
+ /** Set the aboslute offset from the top left position of the label.
+ *
+ * Avoid printing off the edges of a label, which can cause excessive head wear.
+ */
+ setOffset(horizontal: number, vertical?: number): ILabelDocumentBuilder;
+
+ /**
+ * Sets the temporary origin offset from the top-left of the label that all
+ * other offsets are calculated from. Only applies to current label.
+ *
+ * Avoid printing off the edges of a label, which can cause excessive head wear.
+ */
+ setLabelHomeOffsetDots(
+ horizontalOffsetInDots: number,
+ verticalOffsetInDots: number
+ ): ILabelDocumentBuilder;
+
+ /** Add a relative offset to the current offset from the top left position of the label.
+ *
+ * Avoid printing off the edges of a label, which can cause excessive head wear.
+ */
+ addOffset(horizontal: number, vertical?: number): ILabelDocumentBuilder;
+
+ /** Resets the offset back to origin (top left of label) */
+ resetOffset(): ILabelDocumentBuilder;
}
export interface ILabelContentCommandBuilder {
- /** Add an ImageData object as an image to the label */
- addImageFromImageData(
- imageData: ImageData,
- imageConversionOptions?: ImageConversionOptions
- ): ILabelDocumentBuilder;
-
- /** Add a bitmap GRF image to the label */
- addImageFromGRF(image: BitmapGRF): ILabelDocumentBuilder;
-
- /** Add an SVG image to the label, rendered to the given width and height. */
- addImageFromSVG(
- svg: string,
- widthInDots: number,
- heightInDots: number,
- imageConversionOptions?: ImageConversionOptions
- ): Promise;
-
- /** Draw a line from the current offset for the length and height. */
- addLine(
- lengthInDots: number,
- heightInDots: number,
- color?: Commands.DrawColor
- ): ILabelDocumentBuilder;
-
- /** Draw a box from the current offset. */
- addBox(
- lengthInDots: number,
- heightInDots: number,
- thicknessInDots: number
- ): ILabelDocumentBuilder;
+ /** Add an ImageData object as an image to the label */
+ addImageFromImageData(
+ imageData: ImageData,
+ imageConversionOptions?: ImageConversionOptions
+ ): ILabelDocumentBuilder;
+
+ /** Add a bitmap GRF image to the label */
+ addImageFromGRF(image: BitmapGRF): ILabelDocumentBuilder;
+
+ /** Add an SVG image to the label, rendered to the given width and height. */
+ addImageFromSVG(
+ svg: string,
+ widthInDots: number,
+ heightInDots: number,
+ imageConversionOptions?: ImageConversionOptions
+ ): Promise;
+
+ /** Draw a line from the current offset for the length and height. */
+ addLine(
+ lengthInDots: number,
+ heightInDots: number,
+ color?: Commands.DrawColor
+ ): ILabelDocumentBuilder;
+
+ /** Draw a box from the current offset. */
+ addBox(
+ lengthInDots: number,
+ heightInDots: number,
+ thicknessInDots: number
+ ): ILabelDocumentBuilder;
}
diff --git a/src/Documents/ReadyToPrintDocuments.ts b/src/Documents/ReadyToPrintDocuments.ts
index eb60588..53b04a2 100644
--- a/src/Documents/ReadyToPrintDocuments.ts
+++ b/src/Documents/ReadyToPrintDocuments.ts
@@ -1,96 +1,94 @@
import { ConfigDocumentBuilder } from '../Documents/ConfigDocument.js';
import { LabelDocumentBuilder } from '../Documents/LabelDocument.js';
-import { Printer } from '../Printers/Printer.js';
-import { DarknessPercent, PrintSpeed } from '../Printers/Configuration/PrinterOptions.js';
+import { LabelPrinter } from '../Printers/Printer.js';
+import { type DarknessPercent, PrintSpeed } from '../Printers/Configuration/PrinterOptions.js';
/** Collection of handy documents ready to go. */
export class ReadyToPrintDocuments {
- private static readonly printerGetConfigDoc = new ConfigDocumentBuilder(null)
- .queryConfiguration()
- .finalize();
- /** A command document to query the printer for configuration. */
- static get configDocument() {
- return this.printerGetConfigDoc;
- }
+ private static readonly printerGetConfigDoc = new ConfigDocumentBuilder()
+ .queryConfiguration()
+ .finalize();
+ /** A command document to query the printer for configuration. */
+ static get configDocument() {
+ return this.printerGetConfigDoc;
+ }
- /* eslint-disable prettier/prettier */
- private static readonly printerPrintConfigDoc = new ConfigDocumentBuilder(null)
- .printConfiguration();
- /* eslint-enable prettier/prettier */
- /** A command document to make the printer print its configuration. */
- static get printConfigDocument() {
- return this.printerPrintConfigDoc;
- }
+ private static readonly printerPrintConfigDoc = new ConfigDocumentBuilder()
+ .printConfiguration();
+ /** A command document to make the printer print its configuration. */
+ static get printConfigDocument() {
+ return this.printerPrintConfigDoc;
+ }
- private static readonly feedLabelDoc = new LabelDocumentBuilder(null).addPrintCmd(1).finalize();
- /** A label document to feed a single label. */
- static get feedLabelDocument() {
- return this.feedLabelDoc;
- }
+ private static readonly feedLabelDoc = new LabelDocumentBuilder().addPrintCmd(1).finalize();
+ /** A label document to feed a single label. */
+ static get feedLabelDocument() {
+ return this.feedLabelDoc;
+ }
- /**
- * Print a test pattern that looks like
- *
- * ████████████
- *
- * ███
- * ███
- * ███
- * ███
- *
- * ////////////
- *
- * Needs to know the width to adjust the pattern.
- */
- static printTestLabelDocument(labelWidthInDots: number) {
- const label = new LabelDocumentBuilder(null);
- const labelWidth = labelWidthInDots;
- const quarter = labelWidth / 4;
- const lineHeight = 20;
+ /**
+ * Print a test pattern that looks like
+ *
+ * ████████████
+ *
+ * ███
+ * ███
+ * ███
+ * ███
+ *
+ * ////////////
+ *
+ * Needs to know the width to adjust the pattern.
+ */
+ static printTestLabelDocument(labelWidthInDots: number) {
+ const label = new LabelDocumentBuilder();
+ const labelWidth = labelWidthInDots;
+ const quarter = labelWidth / 4;
+ const lineHeight = 20;
- // Blocks
- label
- .resetOffset()
- .addLine(labelWidth, lineHeight * 2)
- .setOffset(0, lineHeight * 2)
- .addLine(quarter, lineHeight)
- .setOffset(quarter, lineHeight * 3)
- .addLine(quarter, lineHeight)
- .setOffset(quarter * 2, lineHeight * 4)
- .addLine(quarter, lineHeight)
- .setOffset(quarter * 3, lineHeight * 5)
- .addLine(quarter, lineHeight)
- .setOffset(0, lineHeight * 6);
+ // Blocks
+ label
+ .resetOffset()
+ .addLine(labelWidth, lineHeight * 2)
+ .setOffset(0, lineHeight * 2)
+ .addLine(quarter, lineHeight)
+ .setOffset(quarter, lineHeight * 3)
+ .addLine(quarter, lineHeight)
+ .setOffset(quarter * 2, lineHeight * 4)
+ .addLine(quarter, lineHeight)
+ .setOffset(quarter * 3, lineHeight * 5)
+ .addLine(quarter, lineHeight)
+ .setOffset(0, lineHeight * 6);
- // Lines
- const slashStart = lineHeight * 6 + 5;
- const slashHeight = 8;
- for (let i = 0; i <= labelWidth; i += 4) {
- label
- .setOffset(i + 0, slashStart + 0)
- .addLine(1, slashHeight)
- .setOffset(i + 1, slashStart + slashHeight)
- .addLine(1, slashHeight)
- .setOffset(i + 2, slashStart + slashHeight * 2)
- .addLine(1, slashHeight)
- .setOffset(i + 3, slashStart + slashHeight * 3)
- .addLine(1, slashHeight);
- }
- return label.addPrintCmd().finalize();
+ // Lines
+ const slashStart = lineHeight * 6 + 5;
+ const slashHeight = 8;
+ for (let i = 0; i <= labelWidth; i += 4) {
+ label
+ .setOffset(i + 0, slashStart + 0)
+ .addLine(1, slashHeight)
+ .setOffset(i + 1, slashStart + slashHeight)
+ .addLine(1, slashHeight)
+ .setOffset(i + 2, slashStart + slashHeight * 2)
+ .addLine(1, slashHeight)
+ .setOffset(i + 3, slashStart + slashHeight * 3)
+ .addLine(1, slashHeight);
}
+ return label.addPrintCmd().finalize();
+ }
- /** Combine the common label settings into one config document. */
- static configLabelSettings(
- printer: Printer,
- labelWidthInches: number,
- darknessPercent: DarknessPercent
- ) {
- return printer
- .getConfigDocument()
- .setPrintDirection()
- .setPrintSpeed(PrintSpeed.ipsAuto)
- .setDarknessConfig(darknessPercent)
- .setLabelDimensions(labelWidthInches)
- .autosenseLabelLength();
- }
+ /** Combine the common label settings into one config document. */
+ static configLabelSettings(
+ printer: LabelPrinter,
+ labelWidthInches: number,
+ darknessPercent: DarknessPercent
+ ) {
+ return printer
+ .getConfigDocument()
+ .setPrintDirection()
+ .setPrintSpeed(PrintSpeed.ipsAuto)
+ .setDarknessConfig(darknessPercent)
+ .setLabelDimensions(labelWidthInches)
+ .autosenseLabelLength();
+ }
}
diff --git a/src/Documents/index.ts b/src/Documents/index.ts
new file mode 100644
index 0000000..933c9e2
--- /dev/null
+++ b/src/Documents/index.ts
@@ -0,0 +1,6 @@
+export * from './BitmapGRF.js'
+export * from './Commands.js'
+export * from './ConfigDocument.js'
+export * from './Document.js'
+export * from './LabelDocument.js'
+export * from './ReadyToPrintDocuments.js'
diff --git a/src/NumericRange.test.ts b/src/NumericRange.test.ts
new file mode 100644
index 0000000..aae0272
--- /dev/null
+++ b/src/NumericRange.test.ts
@@ -0,0 +1,16 @@
+import { test, expect, describe } from 'vitest';
+import { clampToRange } from './NumericRange.js';
+
+describe("clampToRange tests", () => {
+ test('high number is clamped down', () => {
+ expect(clampToRange(5, 0, 2)).toBe(2);
+ });
+
+ test('low number is clamped up', () => {
+ expect(clampToRange(-1, 0, 5)).toBe(0);
+ });
+
+ test('in-range number is not modified', () => {
+ expect(clampToRange(3, 0, 5)).toBe(3);
+ });
+});
diff --git a/src/NumericRange.ts b/src/NumericRange.ts
index a3a06f7..1232b23 100644
--- a/src/NumericRange.ts
+++ b/src/NumericRange.ts
@@ -5,3 +5,30 @@ type Enumerate = Acc['length'] exte
export type NumericRange = Exclude, Enumerate>;
export type Percent = NumericRange<0, 101>;
+
+/** Clamp a number to a given range of values. */
+export function clampToRange(value: number, min: number, max: number) {
+ return Math.min(Math.max(value, min), max);
+}
+
+/** Return a number if it's within an inclusive range, otherwise return the default. */
+export function numberInRange(
+ str: string,
+ min?: number,
+ max?: number) {
+ if (!/^\d+$/.test(str)) {
+ return;
+ }
+ const val = Number(str);
+ if (min !== undefined && val < min) {
+ return;
+ }
+ if (max !== undefined && val > max) {
+ return;
+ }
+ return val;
+}
+
+export function repeat(val: T, count: number) {
+ return new Array(count).fill(val) as T[];
+}
diff --git a/src/PrinterUsbManager.ts b/src/PrinterUsbManager.ts
index b4af721..be26112 100644
--- a/src/PrinterUsbManager.ts
+++ b/src/PrinterUsbManager.ts
@@ -1,130 +1,187 @@
-///
-import { Printer } from './Printers/Printer.js';
-import { PrinterCommunicationOptions } from './Printers/PrinterCommunicationOptions.js';
+import type { IDevice, IDeviceCommunicationOptions, IDeviceEvent } from './Printers/Communication/DeviceCommunication.js';
-export interface PrinterManagerEventMap {
- connectedPrinter: CustomEvent<{ detail: Printer }>;
- disconnectedPrinter: CustomEvent<{ detail: Printer }>;
+export interface IUsbDeviceManagerEventMap {
+ connectedDevice: CustomEvent>;
+ disconnectedDevice: CustomEvent>;
}
-/** Singleton for handling USB printer management.
+export interface IUsbDeviceCommunicationOptions extends IDeviceCommunicationOptions {
+ /** Connection options for what types of USB devices to pay attention to. */
+ requestOptions: USBDeviceRequestOptions;
+}
+
+type DeviceGetter = (
+ device: USBDevice,
+ deviceCommunicationOptions: IUsbDeviceCommunicationOptions
+) => TDevice;
+
+/** Singleton for handling USB device management.
*
* This class can be used to handle the WebUSB communication management for you instead of handling
- * printer communication yourself. The promptToConnect method is used to prompt the user to select
- * a printer using the browser's UI. Once paired at least once the browser will rember and reconnect
+ * device connections yourself. The promptToConnect method is used to prompt the user to select
+ * a device using the browser's UI. Once paired at least once the browser will remember and reconnect
* automatically.
*
* This class exposes events, which your code should add handlers for:
- * * connectedPrinter: Fired when a printer is ready to be interacted with.
- * * disconnectedPrinter: Fired when a printer is no longer connected.
+ * * connectedDevice: Fired when a device is ready to be interacted with.
+ * * disconnectedDevice: Fired when a device is no longer connected.
*
- * This class will bind to WebUSB events on the Navigator element, your code should ensure only
- * one instance is ever instantiated to avoid conflicts.
+ * This class will attempt to manage any USB devices that match the filter you
+ * provide in the constructor. If you instantiate it multiple times you must use
+ * different USBDeviceFilters, otherwise managers will start managing each other's
+ * devices. This will very likely lead to unintended operation.
*/
-export class PrinterUsbManager extends EventTarget {
- private nav: Navigator;
+export class UsbDeviceManager extends EventTarget {
+ private usb: USB;
- /** List of tracked printers. */
- private _printers: Printer[] = [];
- public get printers(): readonly Printer[] {
- return this._printers;
- }
- /** Corresponding list of tracked devices */
- private devices: USBDevice[] = [];
- // TODO: Switch to Record since we use this for a reverse mapping.
+ /** Map of tracked devices to their wrapper objects. */
+ private _devices = new Map();
+ public get devices() { return [...this._devices.values()]; }
+
+ private deviceGetter: DeviceGetter;
+
+ /** Communication behavior when communicating with devices. */
+ public deviceCommunicationOptions: IUsbDeviceCommunicationOptions;
+
+ constructor(
+ navigatorUsb: USB,
+ deviceConstructor: DeviceGetter,
+ commOpts?: IUsbDeviceCommunicationOptions
+ ) {
+ super();
+ this.usb = navigatorUsb;
+ this.deviceGetter = deviceConstructor;
+ this.deviceCommunicationOptions = commOpts ?? {
+ debug: true,
+ requestOptions: {
+ filters: [{
+ vendorId: 0x0a5f // Zebra
+ }]
+ }
+ };
- /** Default comm options used when connecting to a printer. */
- public printerCommunicationOptions: PrinterCommunicationOptions;
+ this.usb.addEventListener('connect', this.handleConnect.bind(this));
+ this.usb.addEventListener('disconnect', this.handleDisconnect.bind(this));
+ }
- constructor(nav: Navigator, printerCommOpts?: PrinterCommunicationOptions) {
- super();
- this.nav = nav;
- this.printerCommunicationOptions = printerCommOpts ?? new PrinterCommunicationOptions();
+ public addEventListener>(
+ type: T,
+ listener: EventListenerObject | null | ((this: UsbDeviceManager, ev: IUsbDeviceManagerEventMap[T]) => void),
+ options?: boolean | AddEventListenerOptions
+ ): void;
+ public addEventListener(
+ type: string,
+ callback: EventListenerOrEventListenerObject | null,
+ options?: boolean | AddEventListenerOptions
+ ): void {
+ super.addEventListener(type, callback, options);
+ }
- // Since this was created assume control over USB.
- this.nav.usb.addEventListener('connect', this.handleConnectPrinter.bind(this));
- this.nav.usb.addEventListener('disconnect', this.handleDisconnectPrinter.bind(this));
+ /** Ask the user to connect to a device, using the filter from deviceCommunicationOptions. */
+ public async promptForNewDevice(): Promise {
+ try {
+ const device = await this.usb.requestDevice(this.deviceCommunicationOptions.requestOptions);
+ await this.handleConnect(new USBConnectionEvent('connect', { device }));
+ } catch (e) {
+ // User cancelled
+ if (
+ e instanceof DOMException &&
+ e.name === 'NotFoundError' &&
+ e.message.endsWith('No device selected.')
+ ) {
+ return false;
+ }
+ throw e;
}
+ return true;
+ }
- public addEventListener(
- type: T,
- listener: (this: PrinterUsbManager, ev: PrinterManagerEventMap[T]) => void,
- options?: boolean | AddEventListenerOptions
- ): void;
- public addEventListener(
- type: string,
- callback: EventListenerOrEventListenerObject,
- options?: boolean | AddEventListenerOptions
- ): void {
- super.addEventListener(type, callback, options);
+ /** Disconnect then reconnect all devices */
+ public async forceReconnect() {
+ const oldList = Array.from(this._devices.values());
+ this._devices.clear();
+ await Promise.all([...oldList].map(async (value) => value.dispose()));
+
+ const newDevices = await this.usb.getDevices();
+ await Promise.all(
+ newDevices
+ .map((d) => new USBConnectionEvent('connect', { device: d }))
+ .map(async (e) => await this.handleConnect(e))
+ );
+ }
+
+ /** Handler for device connection events. */
+ public async handleConnect({ device }: USBConnectionEvent): Promise {
+ // Make sure it's a device this manager cares about.
+ if (!this.isManageableDevice(device)) {
+ // Whatever device this is it isn't one we'd be able to ask the user to
+ // connect to. We shouldn't attempt to talk to it.
+ return;
}
- /** Display the USB device connection dialog to select a printer. */
- public async promptToConnectUsbPrinter(options?: USBDeviceRequestOptions) {
- try {
- const device = await this.nav.usb.requestDevice(
- options ?? {
- filters: [
- {
- vendorId: 0x0a5f // Zebra
- }
- ]
- }
- );
- await this.handleConnectPrinter({ device });
- } catch (e) {
- if (
- e instanceof DOMException &&
- e.name === 'NotFoundError' &&
- e.message === 'No device selected.'
- ) {
- return;
- }
- throw e;
- }
+ // Only handle registration if we aren't already tracking a device
+ let dev = this._devices.get(device)
+ if (dev === undefined) {
+ dev = this.deviceGetter(device, this.deviceCommunicationOptions);
+ this._devices.set(device, dev);
}
- /** Simulate all printers being disconnected and reconnected. */
- public async reconnectAllPrinters() {
- this.devices = [];
- await Promise.all(this._printers.map(async (p) => await p.dispose()));
- this._printers = [];
+ // Don't notify that the device exists until it's ready to exist.
+ await dev.ready;
- const devices = await navigator.usb.getDevices();
- await Promise.all(
- devices.map(async (device) => await this.handleConnectPrinter({ device }))
- );
+ this.sendEvent('connectedDevice', { device: dev });
+ }
+
+ /** Handler for device disconnection events. */
+ public async handleDisconnect({ device }: USBConnectionEvent): Promise {
+ const dev = this._devices.get(device);
+ if (dev === undefined) {
+ return;
}
+ this._devices.delete(device);
+ await dev.dispose();
+
+ this.sendEvent('disconnectedDevice', { device: dev });
+ }
- /** Handler for printer connection events. */
- public async handleConnectPrinter({ device }): Promise {
- // Reconnection events may fire for known printers, exclude them.
- if (this.devices.includes(device)) {
- return;
- }
- this.devices.push(device);
- const printer = Printer.fromUSBDevice(device, this.printerCommunicationOptions);
- this._printers.push(printer);
-
- // Don't notify that the printer exists until it's ready to exist.
- await printer.ready;
-
- const event = new CustomEvent('connectedPrinter', { detail: printer });
- this.dispatchEvent(event);
+ private sendEvent(
+ eventName: keyof IUsbDeviceManagerEventMap,
+ detail: IDeviceEvent
+ ): boolean {
+ return super.dispatchEvent(new CustomEvent>(eventName, { detail }));
+ }
+
+ /** Determine if a given device is allowed to be managed by this manager. */
+ private isManageableDevice(device: USBDevice): boolean {
+ const filters = this.deviceCommunicationOptions.requestOptions.filters;
+ const exclusionFilters = this.deviceCommunicationOptions.requestOptions.exclusionFilters ?? [];
+
+ // Step 1: Look for filters where the device doesn't match.
+ const shouldBeFiltered = filters.map(filter => {
+ return (filter.vendorId !== undefined && filter.vendorId !== device.vendorId)
+ || (filter.productId !== undefined && filter.productId !== device.productId)
+ || (filter.classCode !== undefined && filter.classCode !== device.deviceClass)
+ || (filter.subclassCode !== undefined && filter.subclassCode !== device.deviceSubclass)
+ || (filter.protocolCode !== undefined && filter.protocolCode !== device.deviceProtocol)
+ || (filter.serialNumber !== undefined && filter.serialNumber !== device.serialNumber);
+ });
+ if (shouldBeFiltered.some(r => r === true)) {
+ return false;
}
- /** Handler for printer disconnection events. */
- public async handleDisconnectPrinter({ device }): Promise {
- const idx = this.devices.findIndex((i) => i == device);
- if (idx < 0) {
- return;
- }
- const printer = this._printers[idx];
- this.devices.splice(idx, 1);
- this._printers.splice(idx, 1);
- await printer.dispose();
-
- const event = new CustomEvent('disconnectedPrinter', { detail: printer });
- this.dispatchEvent(event);
+ // Step 2: Look for exclusions where the device does match.
+ const shouldBeExcluded = exclusionFilters.map(filter => {
+ return (filter.vendorId !== undefined && filter.vendorId === device.vendorId)
+ || (filter.productId !== undefined && filter.productId === device.productId)
+ || (filter.classCode !== undefined && filter.classCode === device.deviceClass)
+ || (filter.subclassCode !== undefined && filter.subclassCode === device.deviceSubclass)
+ || (filter.protocolCode !== undefined && filter.protocolCode === device.deviceProtocol)
+ || (filter.serialNumber !== undefined && filter.serialNumber === device.serialNumber);
+ });
+ if (shouldBeExcluded.some(r => r === true)) {
+ return false;
}
+
+ return true;
+ }
}
diff --git a/src/Printers/Communication/DeviceCommunication.ts b/src/Printers/Communication/DeviceCommunication.ts
new file mode 100644
index 0000000..7cf576c
--- /dev/null
+++ b/src/Printers/Communication/DeviceCommunication.ts
@@ -0,0 +1,92 @@
+import { WebZlpError } from "../../WebZlpError.js";
+
+/** Possible ways to communicate with a device */
+export type DeviceChannelType
+ = "USB"
+ | "Serial"
+ | "Bluetooth"
+ | "Network"
+
+/** Whether data can be transmitted or received from the device. */
+export enum ConnectionDirectionMode {
+ none,
+ unidirectional,
+ bidirectional
+}
+
+export interface IDeviceCommunicationOptions {
+ /** Whether to display printer communication to the dev console. */
+ debug: boolean;
+
+ /** Milliseconds to wait for messages from a device before assuming it's done talking. Defaults to 500ms. */
+ messageWaitTimeoutMS?: number
+}
+
+export interface IDevice {
+ /** Close the connection to this device and clean up unmanaged resources. */
+ dispose(): Promise;
+
+ /** Whether the device is connected. */
+ get connected(): boolean;
+
+ /** A promise indicating this device is ready to be used. */
+ ready: Promise;
+}
+
+export interface IDeviceEvent {
+ device: TDevice;
+}
+
+/** Static metadata for a connected device. */
+export interface IDeviceInformation {
+ readonly manufacturerName?: string | undefined;
+ readonly productName?: string | undefined;
+ readonly serialNumber?: string | undefined;
+}
+
+/** A communication channel for talking to a device. */
+export interface IDeviceChannel {
+ /** Gets the mode the communication is set up as. */
+ get commMode(): ConnectionDirectionMode;
+
+ /** Gets this channel type. */
+ readonly channelType: DeviceChannelType;
+
+ /** A promise indicating this communication channel is ready for use. */
+ get ready(): Promise;
+
+ /** Whether the device is connected. */
+ get connected(): boolean;
+
+ /** Close the channel, disallowing future communication. */
+ dispose(): Promise;
+
+ /** Gets the basic information for the device connected on this channel. */
+ getDeviceInfo(): IDeviceInformation
+
+ /**
+ * Send a series of commands to the device.
+ * @param commandBuffer The series of commands to send in order.
+ */
+ sendCommands(commandBuffer: TOutput): Promise;
+
+ /** Request data from the device. */
+ getInput(): Promise;
+}
+
+/** Error indicating communication with the device has failed. */
+export class DeviceCommunicationError extends WebZlpError {
+ constructor(message?: string, innerException?: Error) {
+ super(message ?? innerException?.message ?? 'Error communicating with device');
+ this.innerException = innerException;
+ }
+
+ innerException?: Error;
+}
+
+/** Error indicating the device was not ready to communicate */
+export class DeviceNotReadyError extends DeviceCommunicationError {
+ constructor(message?: string, innerException?: Error) {
+ super(message ?? innerException?.message ?? 'Device not ready to communicate.');
+ }
+}
diff --git a/src/Printers/Communication/LineBreakTransformer.ts b/src/Printers/Communication/LineBreakTransformer.ts
index f47418f..8ea3ecd 100644
--- a/src/Printers/Communication/LineBreakTransformer.ts
+++ b/src/Printers/Communication/LineBreakTransformer.ts
@@ -1,14 +1,14 @@
export class LineBreakTransformer implements Transformer {
- private container = '';
+ private container = '';
- transform(chunk: string, controller: TransformStreamDefaultController) {
- this.container += chunk;
- const lines = this.container.split('\n');
- this.container = lines.pop();
- lines.forEach((line) => controller.enqueue(line));
- }
+ transform(chunk: string, controller: TransformStreamDefaultController) {
+ this.container += chunk;
+ const lines = this.container.split('\n');
+ this.container = lines.pop() ?? '';
+ lines.forEach((line) => controller.enqueue(line));
+ }
- flush(controller: TransformStreamDefaultController) {
- controller.enqueue(this.container);
- }
+ flush(controller: TransformStreamDefaultController) {
+ controller.enqueue(this.container);
+ }
}
diff --git a/src/Printers/Communication/PrinterCommunication.ts b/src/Printers/Communication/PrinterCommunication.ts
deleted file mode 100644
index f876210..0000000
--- a/src/Printers/Communication/PrinterCommunication.ts
+++ /dev/null
@@ -1,58 +0,0 @@
-import { WebZlpError } from '../../WebZlpError.js';
-/** A communication channel for talking to a printer device. */
-export interface IPrinterDeviceChannel {
- /** Whether to print communications to the console. */
- enableConsoleDebug: boolean;
-
- /** Gets the mode the communication is set up as. */
- get commMode(): PrinterCommMode;
-
- /** Gets this channel mode. */
- get channelMode(): PrinterChannelType;
-
- /** Gets the printer model hint, if available. Used to detect config faster. */
- get modelHint(): string;
-
- /** A promise indicating this communication channel is ready for use. */
- get ready(): Promise;
-
- /** Gets the stream for receiving data from this printer. */
- get streamFromPrinter(): ReadableStream;
-
- /** Close the channel, disallowing future communication. */
- dispose(): Promise;
-
- /**
- * Send a series of commands to the printer.
- * @param commandBuffer The series of commands to execute in order.
- */
- sendCommands(commandBuffer: Uint8Array): Promise;
-}
-
-/** Error indicating communication with the printer has failed. */
-export class PrinterCommunicationError extends WebZlpError {
- constructor(message?: string, innerException?: Error) {
- super(message ?? innerException.message);
- this.innerException = innerException;
- }
-
- innerException: Error;
-}
-
-/** Possible ways to communicate with a printer */
-export enum PrinterChannelType {
- /** Printer is connected to the local machine via USB. */
- usb,
- /** Printer is connected to the local machine via Serial. */
- serial,
- /** Printer is connected to the local machine via Bluetooth. */
- bluetooth,
- /** Printer is available on the network. */
- network
-}
-
-export enum PrinterCommMode {
- none,
- unidirectional,
- bidirectional
-}
diff --git a/src/Printers/Communication/UsbPrinterDeviceChannel.ts b/src/Printers/Communication/UsbPrinterDeviceChannel.ts
index 7e59a60..0d25af8 100644
--- a/src/Printers/Communication/UsbPrinterDeviceChannel.ts
+++ b/src/Printers/Communication/UsbPrinterDeviceChannel.ts
@@ -1,213 +1,307 @@
import { WebZlpError } from '../../WebZlpError.js';
+import { ConnectionDirectionMode, DeviceCommunicationError, DeviceNotReadyError, type IDeviceChannel, type IDeviceCommunicationOptions, type IDeviceInformation } from './DeviceCommunication.js';
import { LineBreakTransformer } from './LineBreakTransformer.js';
-import {
- PrinterChannelType,
- IPrinterDeviceChannel,
- PrinterCommMode,
- PrinterCommunicationError
-} from './PrinterCommunication.js';
+
+export interface IUSBDeviceInformation extends IDeviceInformation {
+ readonly deviceClass: number;
+ readonly deviceSubclass: number;
+ readonly deviceProtocol: number;
+ readonly vendorId: number;
+ readonly productId: number;
+ readonly deviceVersionMajor: number;
+ readonly deviceVersionMinor: number;
+ readonly deviceVersionSubminor: number;
+}
+
+function deviceToInfo(device: USBDevice): IUSBDeviceInformation {
+ return {
+ deviceClass: device.deviceClass,
+ deviceProtocol: device.deviceProtocol,
+ deviceSubclass: device.deviceSubclass,
+ deviceVersionMajor: device.deviceVersionMajor,
+ deviceVersionMinor: device.deviceVersionMinor,
+ deviceVersionSubminor: device.deviceVersionSubminor,
+ productId: device.productId,
+ vendorId: device.vendorId,
+ manufacturerName: device.manufacturerName,
+ productName: device.productName,
+ serialNumber: device.serialNumber,
+ };
+}
/** Class for managing the WebUSB communication with a printer. */
-export class UsbPrinterDeviceChannel extends EventTarget implements IPrinterDeviceChannel {
- private device: USBDevice;
- private deviceIn: USBEndpoint;
- private deviceOut: USBEndpoint;
+export class UsbDeviceChannel implements IDeviceChannel {
+ private device: USBDevice;
+ private deviceIn?: USBEndpoint;
+ private deviceOut?: USBEndpoint;
+
+ private _commOptions: IDeviceCommunicationOptions;
+
+ public readonly channelType = "USB" as const;
+
+ private _commMode = ConnectionDirectionMode.none;
+ public get commMode() { return this._commMode; }
+
+ private _readyFlag = false;
+ private _readyPromise: Promise;
+ public get ready() { return this._readyPromise; }
+ public get connected() {
+ return !this._disposed
+ && this._readyFlag
+ && this.device.opened
+ }
+
+ private _disposed = false;
+
+ private _inputStream?: ReadableStream;
- public enableConsoleDebug = false;
+ constructor(
+ device: USBDevice,
+ commOptions: IDeviceCommunicationOptions = { debug: false }
+ ) {
+ this.device = device;
+ this._commOptions = commOptions;
+ this._readyPromise = this.setup();
+ }
- private _commMode: PrinterCommMode;
- public get commMode() {
- return this._commMode;
+ private async setup() {
+ try {
+ await this.connect();
+ } catch {
+ await this.dispose();
}
+ this._readyFlag = true;
+ return true;
+ }
- get channelMode(): PrinterChannelType {
- return PrinterChannelType.usb;
+ public async dispose() {
+ if (this._disposed) {
+ return;
}
- private _ready: Promise;
- public get ready(): Promise {
- return this._ready;
+ this._disposed = true;
+ this._readyPromise = Promise.resolve(false);
+ try {
+ await this.device.close();
+ } catch (e) {
+ if (
+ e instanceof DOMException &&
+ e.name === 'NotFoundError' &&
+ e.message ===
+ "Failed to execute 'close' on 'USBDevice': The device was disconnected."
+ ) {
+ // Device was already closed, no-op.
+ return;
+ }
+
+ throw e;
}
+ }
- private inputStream: ReadableStream;
- public get streamFromPrinter() {
- return this.inputStream;
+ public async sendCommands(
+ commandBuffer: Uint8Array
+ ): Promise {
+ if (this.deviceOut === undefined || !this.connected) {
+ return new DeviceNotReadyError();
}
- public get modelHint(): string {
- return this.device?.productName;
+ if (this._commOptions.debug) {
+ console.debug('Sending command buffer to device via USB.');
+ console.time('commandBufferSendTime');
}
- constructor(device: USBDevice, enableConsoleDebug = false) {
- super();
+ try {
+ // TOOD: Add timeout in case of communication hang.
+ await this.device.transferOut(this.deviceOut.endpointNumber, commandBuffer);
+ return;
+ } catch (e: unknown) {
+ if (typeof e === 'string') {
+ return new DeviceCommunicationError(e);
+ }
+ if (e instanceof Error) {
+ return new DeviceCommunicationError(undefined, e);
+ }
+ // Dunno what this is but we can't wrap it.
+ throw e;
+ } finally {
+ if (this._commOptions.debug) {
+ console.timeEnd('commandBufferSendTime');
+ console.debug('Completed sending commands.');
+ }
+ }
+ }
- this.device = device;
- this.enableConsoleDebug = enableConsoleDebug;
+ private async connect() {
+ const d = this.device;
- this._ready = this.setup();
+ // Most devices have two endpoints on one interface for bidirectional bulk
+ // in and out. The more poorly performing a device the more random this
+ // layout will be, so we must go and look for these two endpoints.
+ if (d.configurations[0]?.interfaces[0]?.alternates[0] === undefined) {
+ // Can't talk to the device at all???
+ throw new DeviceCommunicationError(
+ 'USB device did not expose an endpoint to communicate with. Try power-cycling the device, or checking its settings. This is a hardware problem.'
+ );
}
- private async setup() {
- await this.connect();
- return true;
+ // Open the connections! Stop having it be closed!
+ try {
+ await d.open();
+ } catch (e) {
+ if (
+ e instanceof DOMException &&
+ e.name === 'SecurityError' &&
+ e.message === "Failed to execute 'open' on 'USBDevice': Access denied."
+ ) {
+ // This can happen if something else, usually the operating system, has taken
+ // exclusive access of the USB device and won't allow WebUSB to take control.
+ // This most often happens on Windows. You can use Zadig to replace the driver.
+ throw new DriverAccessDeniedError();
+ }
+
+ throw e;
}
- public async dispose() {
- try {
- await this.device.close();
- } catch (e) {
- if (
- e instanceof DOMException &&
- e.name === 'NotFoundError' &&
- e.message ===
- "Failed to execute 'close' on 'USBDevice': The device was disconnected."
- ) {
- // Device was already closed, no-op.
- return;
- }
+ await d.selectConfiguration(1);
+ await d.claimInterface(0);
- throw e;
- }
+ // A standard Zebra printer will have two endpoints on one interface.
+ // One of them will be output, one of them will be input. They can be
+ // in a random order (or missing!) so we must enumerate them to find them.
+ let o: USBEndpoint | undefined = undefined;
+ let i: USBEndpoint | undefined = undefined;
+ for (const endpoint of d.configurations[0].interfaces[0].alternates[0].endpoints) {
+ if (endpoint.direction == 'out') {
+ o = endpoint;
+ } else if (endpoint.direction == 'in') {
+ i = endpoint;
+ }
}
- public async sendCommands(
- commandBuffer: Uint8Array
- ): Promise {
- if (this.enableConsoleDebug) {
- console.debug('Sending print command buffer to printer via USB..');
- console.time('sendPrintBuffer');
- }
-
- try {
- // TOOD: Add timeout in case of communication hang.
- await this.device.transferOut(this.deviceOut.endpointNumber, commandBuffer);
- return null;
- } catch (e: unknown) {
- if (typeof e === 'string') {
- return new PrinterCommunicationError(e);
- }
- if (e instanceof Error) {
- return new PrinterCommunicationError(null, e);
- }
- // Dunno what this is but we can't wrap it.
- throw e;
- } finally {
- if (this.enableConsoleDebug) {
- console.timeEnd('sendPrintBuffer');
- console.debug('Completed sending print command.');
- }
- }
+ // For no apparent reason sometimes printers will omit to advertise the
+ // input endpoint. Sometimes they'll also omit the output endpoint. This
+ // attempts to handle those situations in a degraded mode.
+ if (!o) {
+ throw new WebZlpError(
+ 'USB printer did not expose an output endpoint. Try power-cycling the printer. This is a hardware problem.'
+ );
+ } else {
+ this.deviceOut = o;
}
- private async connect() {
- const d = this.device;
+ if (!i) {
+ console.warn('USB printer did not expose an input endpoint, using unidirectinal mode.');
+ } else {
+ this.deviceIn = i;
+ }
- // Any sane USB device should expose at least one configuration with one
- // interface.
- if (
- d.configurations.length === 0 ||
- d.configurations[0].interfaces.length === 0 ||
- d.configurations[0].interfaces[0].alternates.length === 0
- ) {
- throw new WebZlpError('USB printer did not expose any USB interfaces. Try power-cycling the printer. This is a hardware problem.');
- }
+ this._commMode = this.getCommMode(this.deviceOut !== undefined, this.deviceIn !== undefined);
+ if (this._commMode === ConnectionDirectionMode.none) {
+ // Can't talk to the printer so don't try.
+ return;
+ }
- // A standard Zebra printer will have two endpoints on one interface.
- // One of them will be output, one of them will be input. They can be
- // in a random order (or missing!) so we must enumerate them to find them.
- let o: USBEndpoint, i: USBEndpoint;
- for (const endpoint of d.configurations[0].interfaces[0].alternates[0].endpoints) {
- if (endpoint.direction == 'out') {
- o = endpoint;
- } else if (endpoint.direction == 'in') {
- i = endpoint;
- }
- }
+ if (this._commOptions.debug) {
+ console.debug('Comm mode with printer is', ConnectionDirectionMode[this._commMode]);
+ }
- // For no apparent reason sometimes printers will omit to advertise the
- // input endpoint. Sometimes they'll also omit the output endpoint. This
- // attempts to handle those situations in a degraded mode.
- if (!o) {
- throw new WebZlpError(
- 'USB printer did not expose an output endpoint. Try power-cycling the printer. This is a hardware problem.'
+ // Can only read if there's an endpoint to read from, otherwise skip.
+ if (this._commMode === ConnectionDirectionMode.bidirectional) {
+ this._inputStream = new ReadableStream({
+ pull: async (controller) => {
+ if (this.deviceIn === undefined || !this.device.opened) {
+ return undefined;
+ }
+ const result = await this.device.transferIn(this.deviceIn.endpointNumber, 64);
+ if (result.data !== undefined) {
+ const chunk = new Uint8Array(
+ result.data.buffer,
+ result.data.byteOffset,
+ result.data.byteLength
);
- } else {
- this.deviceOut = o;
+ controller.enqueue(chunk);
+ }
}
+ })
+ .pipeThrough(new TextDecoderStream())
+ .pipeThrough(new TransformStream(new LineBreakTransformer()));
+ }
+ }
- if (!i) {
- console.warn('USB printer did not expose an input endpoint, using unidirectinal mode.');
- } else {
- this.deviceIn = i;
- }
+ public getDeviceInfo() {
+ return deviceToInfo(this.device);
+ }
- this._commMode = this.getCommMode(this.deviceOut != null, this.deviceIn != null);
- if (this._commMode === PrinterCommMode.none) {
- // Can't talk to the printer so don't try.
- return;
- }
+ public async getInput(): Promise {
+ if (!this.connected || this.deviceIn === undefined) {
+ return new DeviceNotReadyError('Channel is not connected.');
+ }
- if (this.enableConsoleDebug) {
- console.debug('Comm mode with printer is', PrinterCommMode[this._commMode]);
- }
+ let aggregate = '';
+ for (; ;) {
+ const line = await this.nextLine(this._commOptions.messageWaitTimeoutMS ?? 500);
+ if (line === undefined) {
+ this.logIfDebug(`Received ${aggregate.length} long message from printer:\n`, aggregate);
+ return [aggregate];
+ }
+ aggregate += line + '\n';
+ }
+ }
- // Open the connections! Stop having it be closed!
- try {
- await d.open();
- } catch (e) {
- if (
- e instanceof DOMException &&
- e.name === 'SecurityError' &&
- e.message === "Failed to execute 'open' on 'USBDevice': Access denied."
- ) {
- // This can happen if something else, usually the operating system, has taken
- // exclusive access of the USB device and won't allow WebUSB to take control.
- // This most often happens on Windows. You can use Zadig to replace the driver.
- throw new DriverAccessDeniedError();
- }
-
- throw e;
- }
+ /** Wait for the next line of data sent from the printer, or an empty string if nothing is received. */
+ private async nextLine(timeoutMs: number): Promise {
+ if (this._inputStream === undefined) { return; }
+ let reader: ReadableStreamDefaultReader;
+ const nextLinePromise = (async () => {
+ if (this._inputStream === undefined) { return; }
- await d.selectConfiguration(1);
- await d.claimInterface(0);
-
- // Can only read if there's an endpoint to read from, otherwise skip.
- if (this._commMode === PrinterCommMode.bidirectional) {
- this.inputStream = new ReadableStream({
- pull: async (controller) => {
- const result = await this.device.transferIn(this.deviceIn.endpointNumber, 64);
- const chunk = new Uint8Array(
- result.data.buffer,
- result.data.byteOffset,
- result.data.byteLength
- );
- controller.enqueue(chunk);
- }
- })
- .pipeThrough(new TextDecoderStream())
- .pipeThrough(new TransformStream(new LineBreakTransformer()));
- }
+ reader = this._inputStream.getReader();
+ const { value, done } = await reader.read();
+ reader.releaseLock();
+
+ if (done) {
+ return;
+ }
+
+ return value;
+ })();
+
+ const timeoutPromise = new Promise((resolve) => {
+ setTimeout(() => {
+ reader.releaseLock();
+ resolve();
+ }, timeoutMs);
+ });
+
+ return Promise.race([nextLinePromise, timeoutPromise]);
+ }
+
+ private getCommMode(output: boolean, input: boolean) {
+ // TODO: Figure out if getting the Interface Protocol Mode is more
+ // reliable than the detection method used here...
+ if (output === false) {
+ // Can't talk to something that isn't listening...
+ return ConnectionDirectionMode.none;
}
+ if (input === false) {
+ // Can send commands but can't get info back. Operating in the blind.
+ return ConnectionDirectionMode.unidirectional;
+ }
+ return ConnectionDirectionMode.bidirectional;
+ }
- private getCommMode(output: boolean, input: boolean) {
- if (output === false) {
- // No output means we can't control the printer at all.
- return PrinterCommMode.none;
- }
- if (input === false) {
- // No input means we can't listen for feedback, but can send commands.
- return PrinterCommMode.unidirectional;
- }
- return PrinterCommMode.bidirectional;
+ private logIfDebug(...obj: unknown[]) {
+ if (this._commOptions.debug) {
+ console.debug(...obj);
}
+ }
}
/** Error indicating the printer's driver cannot be used by WebUSB. */
export class DriverAccessDeniedError extends WebZlpError {
- constructor() {
- super(
- 'Operating system prevented accessing the USB device. If this is on Windows you may need to replace the driver. See https://cellivar.github.io/WebZLP/docs/windows_driver for more details.'
- );
- }
+ constructor() {
+ super(
+ 'Operating system prevented accessing the USB device. If this is on Windows you may need to replace the driver. See https://cellivar.github.io/WebZLP/docs/windows_driver for more details.'
+ );
+ }
}
diff --git a/src/Printers/Configuration/MediaOptions.ts b/src/Printers/Configuration/MediaOptions.ts
index f4cda9c..3710b98 100644
--- a/src/Printers/Configuration/MediaOptions.ts
+++ b/src/Printers/Configuration/MediaOptions.ts
@@ -1,192 +1,207 @@
-import { Percent } from '../../NumericRange.js';
+import type { Percent } from '../../NumericRange.js';
/** The darkness of the printer setting, higher being printing darker. */
export type DarknessPercent = Percent;
/** Printer options related to the label media being printed */
export interface IPrinterLabelMediaOptions {
- /** How dark to print. 0 is blank, 99 is max darkness */
- darknessPercent: DarknessPercent;
- /** Mode the printer uses to detect separate labels when printing. */
- labelGapDetectMode: LabelMediaGapDetectionMode;
- /**
- * The gap / mark length between labels. Mandatory for markSensing black line mode.
- * Media with webSensing gaps can use AutoSense to get this value.
- */
- get labelGapInches(): number;
- /** Label gap in dots */
- labelGapDots: number;
- /** The offset in inches from the normal location of the label gap or black line. Can be negative. */
- get labelLineOffsetInches(): number;
- /** The offset in dots from the normal location of the label gap or black line. Can be negative. */
- labelLineOffsetDots: number;
- /** The height of the label media, in inches. */
- get labelHeightInches(): number;
- /** The height of the label media, in dots. */
- labelHeightDots: number;
- /** The width of the label media, in inches. */
- get labelWidthInches(): number;
- /** The width of the label media, in dots. */
- labelWidthDots: number;
-
- /** The offset of the printable area, from the top-left corner. */
- labelPrintOriginOffsetDots: Coordinate;
-
- /** Label print speed settings */
- speed: PrintSpeedSettings;
-
- /** The label media thermal print mode. */
- thermalPrintMode: ThermalPrintMode;
-
- /** The behavior of media after form printing. */
- mediaPrintMode: MediaPrintMode;
-
- /** Whether the label prints right-side-up or upside-down. */
- printOrientation: PrintOrientation;
+ /** How dark to print. 0 is blank, 99 is max darkness */
+ darknessPercent: DarknessPercent;
+ /** Mode the printer uses to detect separate labels when printing. */
+ labelGapDetectMode: LabelMediaGapDetectionMode;
+ /**
+ * The gap / mark length between labels. Mandatory for markSensing black line mode.
+ * Media with webSensing gaps can use AutoSense to get this value.
+ */
+ get labelGapInches(): number;
+ /** Label gap in dots */
+ labelGapDots: number;
+ /** The offset in inches from the normal location of the label gap or black line. Can be negative. */
+ get labelLineOffsetInches(): number;
+ /** The offset in dots from the normal location of the label gap or black line. Can be negative. */
+ labelLineOffsetDots: number;
+ /** The height of the label media, in inches. */
+ get labelHeightInches(): number;
+ /** The height of the label media, in dots. */
+ labelHeightDots: number;
+ /** The width of the label media, in inches. */
+ get labelWidthInches(): number;
+ /** The width of the label media, in dots. */
+ labelWidthDots: number;
+
+ /** The offset of the printable area, from the top-left corner. */
+ labelPrintOriginOffsetDots: Coordinate;
+
+ /**
+ * Value to use for rounding read-from-config label sizes.
+ *
+ * When reading the config from a printer the label width and height may be
+ * variable. When you set the label width to 4 inches it's translated into
+ * dots, and then the printer adds a calculated offset to that. This offset
+ * is unique per printer (so far as I have observed) and introduces noise.
+ * This value rounds the returned value to the nearest fraction of an inch.
+ *
+ * For example, with a rounding step of 0.25 (the default) if the printer
+ * returns a width 4.113 it will be rounded to 4.0
+ */
+ labelDimensionRoundingStep: number;
+
+ /** Label print speed settings */
+ speed: PrintSpeedSettings;
+
+ /** The label media thermal print mode. */
+ thermalPrintMode: ThermalPrintMode;
+
+ /** The behavior of media after form printing. */
+ mediaPrintMode: MediaPrintMode;
+
+ /** Whether the label prints right-side-up or upside-down. */
+ printOrientation: PrintOrientation;
}
/** Coordinates on a 2D plane. */
export interface Coordinate {
- /** Offset from the left side of the plane, incrementing to the right. --> */
- left: number;
- /** Offset from the top side of the plane, incrementing down. */
- top: number;
+ /** Offset from the left side of the plane, incrementing to the right. --> */
+ left: number;
+ /** Offset from the top side of the plane, incrementing down. */
+ top: number;
}
/** The orientation of a label as it comes out of the printer. */
export enum PrintOrientation {
- /** Right-side up when the printer faces the user. */
- normal,
- /** Upside-down when the printer faces the user. */
- inverted
+ /** Right-side up when the printer faces the user. */
+ normal,
+ /** Upside-down when the printer faces the user. */
+ inverted
}
/** Configured print speeds for a printer. */
export class PrintSpeedSettings {
- constructor(printSpeed: PrintSpeed, slewSpeed?: PrintSpeed) {
- this.printSpeed = printSpeed;
- this.slewSpeed = slewSpeed ?? printSpeed;
- }
- /** Speed during printing media. */
- printSpeed: PrintSpeed;
- /** Speed during feeding a blank label. ZPL only, same as media speed for EPL. */
- slewSpeed: PrintSpeed;
-
- // My kingdom for extension methods on enums in a reasonable manner.
- /** Look up a speed enum from a given whole number */
- public static getSpeedFromWholeNumber(speed: number): PrintSpeed {
- switch (speed) {
- case 0:
- return PrintSpeed.ipsAuto;
- case 1:
- return PrintSpeed.ips1;
- case 2:
- return PrintSpeed.ips2;
- case 3:
- return PrintSpeed.ips3;
- case 4:
- return PrintSpeed.ips4;
- case 5:
- return PrintSpeed.ips5;
- case 6:
- return PrintSpeed.ips6;
- case 7:
- return PrintSpeed.ips7;
- case 8:
- return PrintSpeed.ips8;
- case 9:
- return PrintSpeed.ips9;
- case 10:
- return PrintSpeed.ips10;
- case 11:
- return PrintSpeed.ips11;
- case 12:
- return PrintSpeed.ips12;
- case 13:
- return PrintSpeed.ips13;
- case 14:
- return PrintSpeed.ips14;
- default:
- return PrintSpeed.unknown;
- }
+ constructor(printSpeed: PrintSpeed, slewSpeed?: PrintSpeed) {
+ this.printSpeed = printSpeed;
+ this.slewSpeed = slewSpeed ?? printSpeed;
+ }
+
+ /** Speed during printing media. */
+ printSpeed: PrintSpeed;
+ /** Speed during feeding a blank label. ZPL only, same as media speed for EPL. */
+ slewSpeed: PrintSpeed;
+
+ // My kingdom for extension methods on enums in a reasonable manner.
+ /** Look up a speed enum from a given whole number */
+ public static getSpeedFromWholeNumber(speed: number): PrintSpeed {
+ switch (speed) {
+ case 0:
+ return PrintSpeed.ipsAuto;
+ case 1:
+ return PrintSpeed.ips1;
+ case 2:
+ return PrintSpeed.ips2;
+ case 3:
+ return PrintSpeed.ips3;
+ case 4:
+ return PrintSpeed.ips4;
+ case 5:
+ return PrintSpeed.ips5;
+ case 6:
+ return PrintSpeed.ips6;
+ case 7:
+ return PrintSpeed.ips7;
+ case 8:
+ return PrintSpeed.ips8;
+ case 9:
+ return PrintSpeed.ips9;
+ case 10:
+ return PrintSpeed.ips10;
+ case 11:
+ return PrintSpeed.ips11;
+ case 12:
+ return PrintSpeed.ips12;
+ case 13:
+ return PrintSpeed.ips13;
+ case 14:
+ return PrintSpeed.ips14;
+ default:
+ return PrintSpeed.unknown;
}
+ }
}
/** Printer speed values in inches per second (IPS). */
export enum PrintSpeed {
- unknown = -1,
- /** Mobile printers can't be configured otherwise. */
- ipsAuto = 0,
- /** The lowest speed a given printer supports. */
- ipsPrinterMin,
- ips1,
- /** EPL-only. Not often supported */
- ips1_5, // eslint-disable-line
- ips2,
- /** EPL-only. Not often supported */
- ips2_5, // eslint-disable-line
- ips3,
- /** EPL-only. Not often supported */
- ips3_5, // eslint-disable-line
- ips4,
- ips5,
- ips6,
- ips7,
- ips8,
- ips9,
- ips10,
- ips11,
- ips12,
- /** Not often supported */
- ips13,
- /** Not often supported */
- ips14,
- /** The highest speed a given printer supports. */
- ipsPrinterMax
+ unknown = -1,
+ /** Mobile printers can't be configured otherwise. */
+ ipsAuto = 0,
+ /** The lowest speed a given printer supports. */
+ ipsPrinterMin,
+ ips1,
+ /** EPL-only. Not often supported */
+ ips1_5, // eslint-disable-line
+ ips2,
+ /** EPL-only. Not often supported */
+ ips2_5, // eslint-disable-line
+ ips3,
+ /** EPL-only. Not often supported */
+ ips3_5, // eslint-disable-line
+ ips4,
+ ips5,
+ ips6,
+ ips7,
+ ips8,
+ ips9,
+ ips10,
+ ips11,
+ ips12,
+ /** Not often supported */
+ ips13,
+ /** Not often supported */
+ ips14,
+ /** The highest speed a given printer supports. */
+ ipsPrinterMax
}
/** The thermal media print mode */
export enum ThermalPrintMode {
- /** Direct thermal with no ribbon. Printer must support this mode. */
- direct,
- /** Thermal transfer, using a ribbon. Printer must support this mode. */
- transfer
+ /** Direct thermal with no ribbon. Printer must support this mode. */
+ direct,
+ /** Thermal transfer, using a ribbon. Printer must support this mode. */
+ transfer
}
/** Describes the way the labels are marked for the printer to detect separate labels. */
export enum LabelMediaGapDetectionMode {
- /** Media is one continous label with no gaps. Used with cutters usually. */
- continuous,
- /** Media is opaque with gaps betwen labels that can be sensed by the printer. */
- webSensing,
- /** Media has black marks indicating label spacing. */
- markSensing,
- /** Autodetect during calibration. G-series printers only. */
- autoDuringCalibration,
- /** KR403 printer only. */
- continuousVariableLength
+ /** Media is one continuous label with no gaps. Used with cutters usually. */
+ continuous,
+ /** Media is opaque with gaps between labels that can be sensed by the printer. */
+ webSensing,
+ /** Media has black marks indicating label spacing. */
+ markSensing,
+ /** Autodetect during calibration. G-series printers only. */
+ autoDuringCalibration,
+ /** KR403 printer only. */
+ continuousVariableLength
}
/** Printing behavior */
export enum MediaPrintMode {
- /** Label advances so web is over tear bar, to be torn manually. */
- tearoff,
- /** Label advances over Label Taken sensor. Printing pauses until label is removed. */
- peel,
- /** Peel mode, but each label is fed to prepeel a small portion. Helps some media types. ZPL only.*/
- peelWithPrepeel,
- /** Peel mode, but printer waits for button tap between labels. */
- peelWithButtonTap,
- /** Label advances until web is over cutter. */
- cutter,
- /** Cutter, but cut operation waits for separate command. ZPL only. */
- cutterWaitForCommand,
- /** Label and liner are rewound on an external device. No backfeed motion. ZPL only. */
- rewind,
- /** Label advances far enough for applicator device to grab. Printers with applicator ports only. */
- applicator,
- /** Removes backfeed between RFID labels, improving throughput. RFID printers only. */
- rfid,
- /** Label is moved into a presentation position. ZPL only.*/
- kiosk
+ /** Label advances so web is over tear bar, to be torn manually. */
+ tearOff,
+ /** Label advances over Label Taken sensor. Printing pauses until label is removed. */
+ peel,
+ /** Peel mode, but each label is fed to pre-peel a small portion. Helps some media types. ZPL only.*/
+ peelWithPrePeel,
+ /** Peel mode, but printer waits for button tap between labels. */
+ peelWithButtonTap,
+ /** Label advances until web is over cutter. */
+ cutter,
+ /** Cutter, but cut operation waits for separate command. ZPL only. */
+ cutterWaitForCommand,
+ /** Label and liner are rewound on an external device. No backfeed motion. ZPL only. */
+ rewind,
+ /** Label advances far enough for applicator device to grab. Printers with applicator ports only. */
+ applicator,
+ /** Removes backfeed between RFID labels, improving throughput. RFID printers only. */
+ rfid,
+ /** Label is moved into a presentation position. ZPL only.*/
+ kiosk
}
diff --git a/src/Printers/Configuration/PrinterOptions.ts b/src/Printers/Configuration/PrinterOptions.ts
index 58f8864..242cd7c 100644
--- a/src/Printers/Configuration/PrinterOptions.ts
+++ b/src/Printers/Configuration/PrinterOptions.ts
@@ -1,126 +1,130 @@
-import { IPrinterModelInfo, UnknownPrinter } from '../Models/PrinterModel.js';
-import * as Serial from './SerialPortSettings.js';
+import { type IPrinterModelInfo, UnknownPrinter } from '../Models/PrinterModel.js';
export * from './SerialPortSettings.js';
import * as Media from './MediaOptions.js';
export * from './MediaOptions.js';
/** Firmware information about the printer that can't be modified. */
export interface IPrinterFactoryInformation {
- /** The raw serial number of the printer. */
- get serialNumber(): string;
- /** The model of the printer. */
- get model(): IPrinterModelInfo;
- /** The firmware version information for the printer. */
- get firmware(): string;
- /** The command languages the printer supports. */
- get language(): PrinterCommandLanguage;
+ /** The raw serial number of the printer. */
+ get serialNumber(): string;
+ /** The model of the printer. */
+ get model(): IPrinterModelInfo;
+ /** The firmware version information for the printer. */
+ get firmware(): string;
+ /** The command languages the printer supports. */
+ get language(): PrinterCommandLanguage;
}
/** Configured options for a label printer */
export class PrinterOptions implements IPrinterFactoryInformation, Media.IPrinterLabelMediaOptions {
- // Read-only printer config info
- private _serialNum: string;
- get serialNumber(): string {
- return this._serialNum;
- }
- private _model: IPrinterModelInfo;
- get model(): IPrinterModelInfo {
- return this._model;
- }
-
- get labelDpi(): number {
- return this._model.dpi;
- }
-
- private _firmware: string;
- get firmware(): string {
- return this._firmware;
- }
-
- get language(): PrinterCommandLanguage {
- return this._model.commandLanguage;
- }
-
- private _valid: boolean;
- get valid(): boolean {
- return this._valid;
- }
-
- speed: Media.PrintSpeedSettings;
-
- darknessPercent: Media.DarknessPercent;
- thermalPrintMode: Media.ThermalPrintMode;
- mediaPrintMode: Media.MediaPrintMode;
- printOrientation: Media.PrintOrientation;
- labelGapDetectMode: Media.LabelMediaGapDetectionMode;
- labelPrintOriginOffsetDots: Media.Coordinate;
-
- labelGapDots: number;
- get labelGapInches() {
- return this.dotToInch(this.labelGapDots);
- }
-
- labelLineOffsetDots: number;
- get labelLineOffsetInches() {
- return this.dotToInch(this.labelLineOffsetDots);
- }
-
- labelWidthDots: number;
- get labelWidthInches() {
- return this.dotToInch(this.labelWidthDots);
- }
- labelHeightDots: number;
- get labelHeightInches() {
- return this.dotToInch(this.labelHeightDots);
- }
-
- constructor(serialNumber: string, model: IPrinterModelInfo, firmware: string, valid = true) {
- this._serialNum = serialNumber;
- this._model = model;
- this._firmware = firmware;
- this._valid = valid;
- }
-
- /** Get a default invalid config. */
- public static invalid() {
- return new PrinterOptions('', new UnknownPrinter(), '', false);
- }
-
- private dotToInch(dots: number) {
- return Math.round((dots / this.model.dpi) * 100 + Number.EPSILON) / 100;
- }
-
- public copy(): PrinterOptions {
- const copy = new PrinterOptions(this.serialNumber, this.model, this.firmware, this.valid);
- copy.printOrientation = this.printOrientation;
- copy.speed = this.speed;
- copy.darknessPercent = this.darknessPercent;
- copy.thermalPrintMode = this.thermalPrintMode;
- copy.mediaPrintMode = this.mediaPrintMode;
- copy.labelGapDetectMode = this.labelGapDetectMode;
- copy.labelPrintOriginOffsetDots = this.labelPrintOriginOffsetDots;
- copy.labelGapDots = this.labelGapDots;
- copy.labelWidthDots = this.labelWidthDots;
- copy.labelHeightDots = this.labelHeightDots;
-
- return copy;
- }
+ // Read-only printer config info
+ private _serialNum: string;
+ get serialNumber(): string {
+ return this._serialNum;
+ }
+ private _model: IPrinterModelInfo;
+ get model(): IPrinterModelInfo {
+ return this._model;
+ }
+
+ get labelDpi(): number {
+ return this._model.dpi;
+ }
+
+ private _firmware: string;
+ get firmware(): string {
+ return this._firmware;
+ }
+
+ get language(): PrinterCommandLanguage {
+ return this._model.commandLanguage;
+ }
+
+ private _valid: boolean;
+ get valid(): boolean {
+ return this._valid;
+ }
+
+ public labelDimensionRoundingStep = 0.25;
+
+ speed: Media.PrintSpeedSettings = new Media.PrintSpeedSettings(Media.PrintSpeed.unknown);
+ darknessPercent: Media.DarknessPercent = 50;
+ thermalPrintMode = Media.ThermalPrintMode.direct;
+ mediaPrintMode = Media.MediaPrintMode.tearOff;
+ printOrientation = Media.PrintOrientation.normal;
+ labelGapDetectMode = Media.LabelMediaGapDetectionMode.webSensing;
+ labelPrintOriginOffsetDots: Media.Coordinate = { left: 0, top: 0 };
+
+ labelGapDots: number = 0;
+ get labelGapInches() {
+ return this.dotToInch(this.labelGapDots);
+ }
+
+ labelLineOffsetDots: number = 0;
+ get labelLineOffsetInches() {
+ return this.dotToInch(this.labelLineOffsetDots);
+ }
+
+ labelWidthDots: number = 100;
+ get labelWidthInches() {
+ return this.dotToInch(this.labelWidthDots);
+ }
+ labelHeightDots: number = 100;
+ get labelHeightInches() {
+ return this.dotToInch(this.labelHeightDots);
+ }
+
+ constructor(
+ serialNumber: string,
+ model: IPrinterModelInfo,
+ firmware: string,
+ valid = true
+ ) {
+ this._serialNum = serialNumber;
+ this._model = model;
+ this._firmware = firmware;
+ this._valid = valid;
+ }
+
+ /** Get a default invalid config. */
+ public static readonly invalid = new PrinterOptions('', new UnknownPrinter(), '', false);
+
+ private dotToInch(dots?: number) {
+ if (dots === undefined || this.model.dpi === undefined) { return 0; }
+ return Math.round((dots / this.model.dpi) * 100 + Number.EPSILON) / 100;
+ }
+
+ public copy(): PrinterOptions {
+ const copy = new PrinterOptions(this.serialNumber, this.model, this.firmware, this.valid);
+ copy.printOrientation = this.printOrientation;
+ copy.speed = this.speed;
+ copy.darknessPercent = this.darknessPercent;
+ copy.thermalPrintMode = this.thermalPrintMode;
+ copy.mediaPrintMode = this.mediaPrintMode;
+ copy.labelGapDetectMode = this.labelGapDetectMode;
+ copy.labelPrintOriginOffsetDots = this.labelPrintOriginOffsetDots;
+ copy.labelGapDots = this.labelGapDots;
+ copy.labelWidthDots = this.labelWidthDots;
+ copy.labelHeightDots = this.labelHeightDots;
+
+ return copy;
+ }
}
// [flags] I miss C#.
/** Command languages a printer could support. One printer may support multiple. */
export enum PrinterCommandLanguage {
- /** Error condition indicating autodetect failed. */
- none = 0,
- /** Printer can be set to EPL mode. */
- epl = 1 << 0,
- /** Printer can be set to ZPL mode. */
- zpl = 1 << 1,
- /** Printer can be set to CPCL mode. */
- cpcl = 1 << 2,
-
- /** Printer is capable of switching between EPL and ZPL. */
- zplEmulateEpl = epl | zpl,
- /** Printer is CPCL native and can emulate EPL and ZPL. */
- cpclEmulateBoth = cpcl | epl | zpl
+ /** Error condition indicating autodetect failed. */
+ none = 0,
+ /** Printer can be set to EPL mode. */
+ epl = 1 << 0,
+ /** Printer can be set to ZPL mode. */
+ zpl = 1 << 1,
+ /** Printer can be set to CPCL mode. */
+ cpcl = 1 << 2,
+
+ /** Printer is capable of switching between EPL and ZPL. */
+ zplEmulateEpl = epl | zpl,
+ /** Printer is CPCL native and can emulate EPL and ZPL. */
+ cpclEmulateBoth = cpcl | epl | zpl
}
diff --git a/src/Printers/Configuration/SerialPortSettings.ts b/src/Printers/Configuration/SerialPortSettings.ts
index 2db1c7b..1dc19eb 100644
--- a/src/Printers/Configuration/SerialPortSettings.ts
+++ b/src/Printers/Configuration/SerialPortSettings.ts
@@ -1,80 +1,80 @@
/** The serial port settings for a printer */
-export class SerialPortSettings {
- /** Port baud rate. Default s9600. */
- public speed: SerialPortSpeed;
- /** Port parity. Default none. */
- public parity: SerialPortParity;
- /** Data bit count. Default eight. */
- public dataBits: SerialPortDataBits;
- /** Stop bit count. Default one. */
- public stopBits: SerialPortStopBits;
- /** Handshake mode. Default XON/XOFF. ZPL only. */
- public handshake?: SerialPortHandshake;
- /** Error protocol. Default none. ZPL only. */
- public errorProtocol?: SerialPortZebraProtocol;
- /** Multi-drop serial network ID, between 000 and 999. Default 000. ZPL only. */
- public networkId?: number;
+export interface SerialPortSettings {
+ /** Port baud rate. Default s9600. */
+ speed: SerialPortSpeed;
+ /** Port parity. Default none. */
+ parity: SerialPortParity;
+ /** Data bit count. Default eight. */
+ dataBits: SerialPortDataBits;
+ /** Stop bit count. Default one. */
+ stopBits: SerialPortStopBits;
+ /** Handshake mode. Default XON/XOFF. ZPL only. */
+ handshake?: SerialPortHandshake;
+ /** Error protocol. Default none. ZPL only. */
+ errorProtocol?: SerialPortZebraProtocol;
+ /** Multi-drop serial network ID, between 000 and 999. Default 000. ZPL only. */
+ networkId?: number;
}
/** Baud rate of the serial port. Not all printers support all speeds. */
export enum SerialPortSpeed {
- /** Not commonly supported. */
- s110 = 110,
+ /** Not commonly supported. */
+ s110 = 110,
/** ZPL only */
- s300 = 300,
+ s300 = 300,
/** ZPL only */
- s600 = 600,
- s1200 = 1200,
- s2400 = 2400,
- s4800 = 4800,
- s9600 = 9600,
- s14400 = 14400,
- s19200 = 19200,
- s28800 = 28800,
- s38400 = 38400,
+ s600 = 600,
+ s1200 = 1200,
+ s2400 = 2400,
+ s4800 = 4800,
+ s9600 = 9600,
+ s14400 = 14400,
+ s19200 = 19200,
+ s28800 = 28800,
+ s38400 = 38400,
/** Not all printers */
- s57600 = 57600,
+ s57600 = 57600,
/** Not all printers */
- s115200 = 115200
+ s115200 = 115200
}
/** Parity of the serial port */
export enum SerialPortParity {
- none,
- odd,
- even
+ none,
+ odd,
+ even
}
/** Number of serial data bits */
export enum SerialPortDataBits {
- seven = 7,
- eight = 8
+ seven = 7,
+ eight = 8
}
/** Number of serial stop bits */
export enum SerialPortStopBits {
- one = 1,
- two = 2
+ one = 1,
+ two = 2
}
/** Serial protocol flow control mode. ZPL only. */
export enum SerialPortHandshake {
- /** Software flow control */
- xon_xoff, //eslint-disable-line
- /** Hardware flow control */
- dtr_dsr, //eslint-disable-line
- /** Hardware pacing control */
- rts_cts, //eslint-disable-line
- /** Auto-detect flow control based on first flow control detected. G-series printers only */
- dtr_dsr_and_xon_xoff //eslint-disable-line
+ /** Software flow control */
+ xon_xoff, //eslint-disable-line
+ /** Hardware flow control */
+ dtr_dsr, //eslint-disable-line
+ /** Hardware pacing control */
+ rts_cts, //eslint-disable-line
+ /** Auto-detect flow control based on first flow control detected. G-series printers only */
+ dtr_dsr_and_xon_xoff //eslint-disable-line
}
/** Error checking protocol. You probably want this to always be none. ZPL only. */
export enum SerialPortZebraProtocol {
- /** No error checking handshake. Default. */
- none,
- /** Send ACK/NAK packets back to host. */
- ack_nak, //eslint-disable-line
- /** ack_nak with sequencing. Requires DSR/DTR. */
- zebra
+ /** No error checking handshake. Default. */
+ none,
+ /** Send ACK/NAK packets back to host. */
+ ack_nak, //eslint-disable-line
+ /** ack_nak with sequencing. Requires DSR/DTR. */
+ zebra
}
diff --git a/test/Printers/Languages/EplPrinterCommandSet.test.ts b/src/Printers/Languages/EplPrinterCommandSet.test.ts
similarity index 83%
rename from test/Printers/Languages/EplPrinterCommandSet.test.ts
rename to src/Printers/Languages/EplPrinterCommandSet.test.ts
index 6106371..3195dca 100644
--- a/test/Printers/Languages/EplPrinterCommandSet.test.ts
+++ b/src/Printers/Languages/EplPrinterCommandSet.test.ts
@@ -1,10 +1,10 @@
-///
+import { expect, describe, it } from 'vitest';
import {
AddImageCommand,
EplPrinterCommandSet,
- TranspilationFormMetadata
-} from '../../../src/index.js';
-import { BitmapGRF } from '../../../src/Documents/BitmapGRF.js';
+ TranspiledDocumentState
+} from '../../index.js';
+import { BitmapGRF } from '../../Documents/BitmapGRF.js';
// Class pulled from jest-mock-canvas which I can't seem to actually import.
class ImageData {
@@ -27,7 +27,7 @@ class ImageData {
return 'srgb' as PredefinedColorSpace;
}
- constructor(arr, w, h) {
+ constructor(arr: number | Uint8ClampedArray, w: number, h?: number) {
if (arguments.length === 2) {
if (arr instanceof Uint8ClampedArray) {
if (arr.length === 0)
@@ -50,7 +50,7 @@ class ImageData {
this._height = height;
this._data = new Uint8ClampedArray(width * height * 4);
}
- } else if (arguments.length === 3) {
+ } else if (arguments.length === 3 && h !== undefined) {
if (!(arr instanceof Uint8ClampedArray))
throw new TypeError('First argument must be a Uint8ClampedArray when using 3 arguments.');
if (arr.length === 0) throw new RangeError('Source length must be a positive multiple of 4.');
@@ -73,7 +73,7 @@ class ImageData {
function getImageDataInput(width: number, height: number, fill: number, alpha?: number) {
const arr = new Uint8ClampedArray(width * height * 4);
- if (alpha != null && alpha != fill) {
+ if (alpha !== undefined && alpha != fill) {
for (let i = 0; i < arr.length; i += 4) {
arr[i + 0] = fill;
arr[i + 1] = fill;
@@ -88,13 +88,13 @@ function getImageDataInput(width: number, height: number, fill: number, alpha?:
const cmdSet = new EplPrinterCommandSet();
-describe('EplImageConversionToFullCommand', () => {
+describe('EPL Image Conversion', () => {
it('Should convert blank images to valid command', () => {
const imageData = new ImageData(getImageDataInput(8, 1, 0), 8, 1);
const bitmap = BitmapGRF.fromCanvasImageData(imageData, { trimWhitespace: false });
const cmd = new AddImageCommand(bitmap, {});
- const doc = new TranspilationFormMetadata();
- const resultCmd = cmdSet['addImageCommand'](cmd, doc, cmdSet);
+ const doc = new TranspiledDocumentState();
+ const resultCmd = cmdSet['addImageCommand'](cmd, doc);
const expectedCmd = Uint8Array.from([
...new TextEncoder().encode('GW0,0,1,1,'),
@@ -110,10 +110,10 @@ describe('EplImageConversionToFullCommand', () => {
const bitmap = BitmapGRF.fromCanvasImageData(imageData, { trimWhitespace: false });
const cmd = new AddImageCommand(bitmap, {});
const appliedOffset = 10;
- const doc = new TranspilationFormMetadata();
+ const doc = new TranspiledDocumentState();
doc.horizontalOffset = appliedOffset;
doc.verticalOffset = appliedOffset * 2;
- const resultCmd = cmdSet['addImageCommand'](cmd, doc, cmdSet);
+ const resultCmd = cmdSet['addImageCommand'](cmd, doc);
const expectedCmd = Uint8Array.from([
...new TextEncoder().encode(`GW${appliedOffset},${appliedOffset * 2},1,1,`),
@@ -123,11 +123,4 @@ describe('EplImageConversionToFullCommand', () => {
expect(resultCmd).toEqual(expectedCmd);
});
-
- it('Should return noop for blank imageData', () => {
- const doc = new TranspilationFormMetadata();
- const resultCmd = cmdSet['addImageCommand'](null, doc, cmdSet);
-
- expect(resultCmd).toEqual(new Uint8Array());
- });
});
diff --git a/src/Printers/Languages/EplPrinterCommandSet.ts b/src/Printers/Languages/EplPrinterCommandSet.ts
index f2d9d14..13faafd 100644
--- a/src/Printers/Languages/EplPrinterCommandSet.ts
+++ b/src/Printers/Languages/EplPrinterCommandSet.ts
@@ -1,569 +1,572 @@
+/* eslint-disable no-fallthrough */
import { WebZlpError } from '../../WebZlpError.js';
import * as Options from '../Configuration/PrinterOptions.js';
import { PrinterOptions } from '../Configuration/PrinterOptions.js';
import { PrinterModelDb } from '../Models/PrinterModelDb.js';
import { PrinterModel } from '../Models/PrinterModel.js';
import {
- CommandFormInclusionMode,
- PrinterCommandSet,
- TranspilationFormMetadata,
- TranspileCommandDelegate
+ PrinterCommandSet,
+ TranspiledDocumentState,
+ type IPrinterExtendedCommandMapping,
+ exhaustiveMatchGuard
} from './PrinterCommandSet.js';
import * as Commands from '../../Documents/Commands.js';
-import { PrinterCommunicationOptions } from '../PrinterCommunicationOptions.js';
+import { clampToRange } from '../../NumericRange.js';
/** Command set for communicating with an EPL II printer. */
export class EplPrinterCommandSet extends PrinterCommandSet {
- private encoder = new TextEncoder();
-
- get commandLanguage(): Options.PrinterCommandLanguage {
- return Options.PrinterCommandLanguage.epl;
- }
-
- get formStartCommand(): Uint8Array {
- // Start of any EPL document should include a clear image buffer to prevent
- // previous commands from affecting the document.
- return this.encodeCommand('\r\nN');
- }
-
- get formEndCommand(): Uint8Array {
- // There's no formal command for the end of an EPL doc, but just in case
- // add a newline.
- return this.encodeCommand();
- }
-
- protected nonFormCommands: (symbol | Commands.CommandType)[] = [
- Commands.CommandType.AutosenseLabelDimensionsCommand,
- Commands.CommandType.PrintConfigurationCommand,
- Commands.CommandType.QueryConfigurationCommand,
- Commands.CommandType.RawDocumentCommand,
- Commands.CommandType.RebootPrinterCommand
- ];
-
- protected transpileCommandMap = new Map<
- symbol | Commands.CommandType,
- TranspileCommandDelegate
- >([
- /* eslint-disable prettier/prettier */
- // Ghost commands which shouldn't make it this far.
- [Commands.CommandType.NewLabelCommand, this.unprocessedCommand],
- [Commands.CommandType.CommandCustomSpecificCommand, this.unprocessedCommand],
- [Commands.CommandType.CommandLanguageSpecificCommand, this.unprocessedCommand],
- // Actually valid commands to parse
- [Commands.CommandType.OffsetCommand, this.modifyOffset],
- [Commands.CommandType.ClearImageBufferCommand, () => this.formStartCommand],
- [Commands.CommandType.CutNowCommand, () => this.encodeCommand('C')],
+ private encoder = new TextEncoder();
+
+ get commandLanguage(): Options.PrinterCommandLanguage {
+ return Options.PrinterCommandLanguage.epl;
+ }
+
+ get formStartCommand(): Uint8Array {
+ // Start of any EPL document should include a clear image buffer to prevent
+ // previous commands from affecting the document.
+ return this.encodeCommand('\r\nN');
+ }
+
+ get formEndCommand(): Uint8Array {
+ // There's no formal command for the end of an EPL doc, but just in case
+ // add a newline.
+ return this.encodeCommand();
+ }
+
+ protected nonFormCommands: (symbol | Commands.CommandType)[] = [
+ 'AutosenseLabelDimensionsCommand',
+ 'PrintConfigurationCommand',
+ 'QueryConfigurationCommand',
+ 'RawDocumentCommand',
+ 'RebootPrinterCommand'
+ ];
+
+ constructor(
+ extendedCommands: Array> = []
+ ) {
+ super(Options.PrinterCommandLanguage.epl, extendedCommands);
+ }
+
+ public encodeCommand(str = '', withNewline = true): Uint8Array {
+ // Every command in EPL ends with a newline.
+ return this.encoder.encode(str + (withNewline ? '\r\n' : ''));
+ }
+
+ public transpileCommand(
+ cmd: Commands.IPrinterCommand,
+ docState: TranspiledDocumentState
+ ): Uint8Array {
+ switch (cmd.type) {
+ default:
+ exhaustiveMatchGuard(cmd.type);
+ break;
+ case 'CustomCommand':
+ return this.extendedCommandHandler(cmd, docState);
+ case 'NewLabelCommand':
+ // Should have been compiled out at a higher step.
+ return this.unprocessedCommand(cmd);
+
+ case 'RebootPrinterCommand':
+ return this.encodeCommand('^@');
+ case 'QueryConfigurationCommand':
+ return this.encodeCommand('UQ');
+ case 'PrintConfigurationCommand':
+ return this.encodeCommand('U');
+ case 'SaveCurrentConfigurationCommand':
+ // EPL doesn't have an explicit save step.
+ return this.noop;
+
+ case 'SetPrintDirectionCommand':
+ return this.setPrintDirectionCommand((cmd as Commands.SetPrintDirectionCommand).upsideDown);
+ case 'SetDarknessCommand':
+ return this.setDarknessCommand((cmd as Commands.SetDarknessCommand).darknessSetting);
+ case 'AutosenseLabelDimensionsCommand':
+ return this.encodeCommand('xa');
+ case 'SetPrintSpeedCommand':
+ // EPL has no separate media slew speed setting.
+ return this.setPrintSpeedCommand((cmd as Commands.SetPrintSpeedCommand).speedVal);
+ case 'SetLabelDimensionsCommand':
+ return this.setLabelDimensionsCommand(cmd as Commands.SetLabelDimensionsCommand);
+ case 'SetLabelHomeCommand':
+ return this.setLabelHomeCommand(cmd as Commands.SetLabelHomeCommand, docState, this);
+ case 'SetLabelPrintOriginOffsetCommand':
+ return this.setLabelPrintOriginOffsetCommand(cmd as Commands.SetLabelPrintOriginOffsetCommand);
+ case 'SetLabelToContinuousMediaCommand':
+ return this.setLabelToContinuousMediaCommand(cmd as Commands.SetLabelToContinuousMediaCommand);
+ case 'SetLabelToMarkMediaCommand':
+ return this.setLabelToMarkMediaCommand(cmd as Commands.SetLabelToMarkMediaCommand);
+ case 'SetLabelToWebGapMediaCommand':
+ return this.setLabelToWebGapMediaCommand(cmd as Commands.SetLabelToWebGapMediaCommand);
+
+ case 'ClearImageBufferCommand':
+ // Clear image buffer isn't a relevant command on ZPL printers.
+ // Closest equivalent is the ~JP (pause and cancel) or ~JA (cancel all) but both
+ // affect in-progress printing operations which is unlikely to be desired operation.
+ // Translate as a no-op.
+ return this.formStartCommand;
+ case 'SuppressFeedBackupCommand':
// EPL uses an on/off style for form backup, it'll remain off until reenabled.
- [Commands.CommandType.SuppressFeedBackupCommand, () => this.encodeCommand('JB')],
+ return this.encodeCommand('JB');
+ case 'EnableFeedBackupCommand':
// Thus EPL needs an explicit command to re-enable.
- [Commands.CommandType.EnableFeedBackupCommand, () => this.encodeCommand('JF')],
- [Commands.CommandType.RebootPrinterCommand, () => this.encodeCommand('^@')],
- [Commands.CommandType.QueryConfigurationCommand, () => this.encodeCommand('UQ')],
- [Commands.CommandType.PrintConfigurationCommand, () => this.encodeCommand('U')],
- [Commands.CommandType.SaveCurrentConfigurationCommand, () => this.noop],
- [Commands.CommandType.SetPrintDirectionCommand, this.setPrintDirectionCommand],
- [Commands.CommandType.SetDarknessCommand, this.setDarknessCommand],
- [Commands.CommandType.SetPrintSpeedCommand, this.setPrintSpeedCommand],
- [Commands.CommandType.AutosenseLabelDimensionsCommand, () => this.encodeCommand('xa')],
- [Commands.CommandType.SetLabelDimensionsCommand, this.setLabelDimensionsCommand],
- [Commands.CommandType.SetLabelHomeCommand, this.setLabelHomeCommand],
- [Commands.CommandType.SetLabelPrintOriginOffsetCommand, this.setLabelPrintOriginOffsetCommand],
- [Commands.CommandType.SetLabelToContinuousMediaCommand, this.setLabelToContinuousMediaCommand],
- [Commands.CommandType.SetLabelToWebGapMediaCommand, this.setLabelToWebGapMediaCommand],
- [Commands.CommandType.SetLabelToMarkMediaCommand, this.setLabelToMarkMediaCommand],
- [Commands.CommandType.AddImageCommand, this.addImageCommand],
- [Commands.CommandType.AddLineCommand, this.addLineCommand],
- [Commands.CommandType.AddBoxCommand, this.addBoxCommand],
- [Commands.CommandType.PrintCommand, this.printCommand]
- /* eslint-enable prettier/prettier */
- ]);
-
- constructor(
- customCommands: Array<{
- commandType: symbol;
- applicableLanguages: Options.PrinterCommandLanguage;
- transpileDelegate: TranspileCommandDelegate;
- commandInclusionMode: CommandFormInclusionMode;
- }> = []
- ) {
- super();
-
- for (const newCmd of customCommands) {
- if ((newCmd.applicableLanguages & this.commandLanguage) !== this.commandLanguage) {
- // Command declared to not be applicable to this command set, skip it.
- continue;
- }
-
- this.transpileCommandMap.set(newCmd.commandType, newCmd.transpileDelegate);
- if (newCmd.commandInclusionMode !== CommandFormInclusionMode.sharedForm) {
- this.nonFormCommands.push(newCmd.commandType);
- }
- }
+ return this.encodeCommand('JF');
+
+ case 'OffsetCommand':
+ return this.modifyOffset(cmd as Commands.OffsetCommand, docState, this);
+ case 'RawDocumentCommand':
+ return this.encodeCommand((cmd as Commands.RawDocumentCommand).rawDocument, false);
+ case 'AddBoxCommand':
+ return this.addBoxCommand(cmd as Commands.AddBoxCommand, docState);
+ case 'AddImageCommand':
+ return this.addImageCommand(cmd as Commands.AddImageCommand, docState);
+ case 'AddLineCommand':
+ return this.addLineCommand(cmd as Commands.AddLineCommand, docState);
+ case 'CutNowCommand':
+ return this.encodeCommand('C');
+
+ case 'PrintCommand':
+ return this.printCommand(cmd as Commands.PrintCommand);
}
-
- public encodeCommand(str = '', withNewline = true): Uint8Array {
- // Every command in EPL ends with a newline.
- return this.encoder.encode(str + (withNewline ? '\r\n' : ''));
+ }
+
+ public parseConfigurationResponse(
+ rawText: string,
+ mediaOptions: Options.IPrinterLabelMediaOptions,
+ ): PrinterOptions {
+ // Raw text from the printer contains \r\n, normalize to \n.
+ const lines = rawText
+ .replaceAll('\r', '')
+ .split('\n')
+ .filter((i) => i);
+
+ if (lines.length <= 0) {
+ // No config provided, can't make a valid config out of it.
+ return PrinterOptions.invalid;
}
- parseConfigurationResponse(
- rawText: string,
- commOpts: PrinterCommunicationOptions
- ): PrinterOptions {
- // Raw text from the printer contains \r\n, normalize to \n.
- const lines = rawText
- .replaceAll('\r', '')
- .split('\n')
- .filter((i) => i);
-
- if (lines.length <= 0) {
- // No config provided, can't make a valid config out of it.
- return PrinterOptions.invalid();
- }
-
- // We make a lot of assumptions about the format of the config output.
- // Unfortunately EPL-only printers tended to have a LOT of variance on
- // what they actually put into the config. Firmware versions, especially
- // shipper-customzied versions, can and do omit information.
- // This method attempts to get what we can out of it.
-
- // See the docs folder for more information on this format.
-
- // First line determines firmware version and model number. When splitting
- // the string by spaces the last element should always be the version and
- // the rest of the elements are the model number.
- // UKQ1935HLU V4.29 // Normal LP244
- // UKQ1935HMU FDX V4.45 // FedEx modified LP2844
- // UKQ1935H U UPS V4.14 // UPS modified LP2844
- const header = lines[0].split(' ').filter((i) => i);
- const firmwareVersion = header.pop();
- const rawModelId = header.join(' ');
-
- const printerInfo = {
- model: PrinterModelDb.getModel(rawModelId),
- firmware: firmwareVersion,
- serial: 'no_serial_nm',
- serialPort: undefined,
- speed: undefined,
- doubleBuffering: undefined,
- headDistanceIn: undefined,
- printerDistanceIn: undefined,
- hardwareOptions: []
- };
-
- const labelInfo = {
- labelWidthDots: undefined,
- labelGapDots: undefined,
- labelGapOffsetDots: undefined,
- labelHeightDots: undefined,
- density: undefined,
- xRef: undefined,
- yRef: undefined,
- orientation: undefined,
- mediaMode: undefined
- };
-
- // All the rest of these follow some kind of standard pattern for
- // each value which we can pick up with regex. The cases here are
- // built out of observed configuration dumps.
- for (let i = 1; i < lines.length; i++) {
- const str = lines[i];
- switch (true) {
- case /^S\/N.*/.test(str):
- // S/N: 42A000000000 # Serial number
- printerInfo.serial = str.substring(5).trim();
- break;
- case /^Serial\sport/.test(str):
- // Serial port:96,N,8,1 # Serial port config
- printerInfo.serialPort = str.substring(12).trim();
- break;
- case /^q\d+\sQ/.test(str): {
- // q600 Q208,25 # Form width (q) and length (Q), with label gap
- const settingsForm = str.trim().split(' ');
- // Label width includes 4 dots of padding. Ish. Maybe.
- labelInfo.labelWidthDots = parseInt(settingsForm[0].substring(1)) - 4;
- // Length is fuzzy, depending on the second value this can be
- // A: The length of the label surface
- // B: The distance between black line marks
- // C: The length of the form on continous media
- // Format is Qp1,p2[,p3]
- const length = settingsForm[1].split(',');
- // p1 is always present and can be treated as the 'label height' consistently.
- labelInfo.labelHeightDots = parseInt(length[0].substring(1));
- // p2 value depends on...
- const rawGapMode = length[1].trim();
- if (rawGapMode === '0') {
- // Length of '0' indicates continuous media.
- labelInfo.mediaMode = Options.LabelMediaGapDetectionMode.continuous;
- } else if (rawGapMode.startsWith('B')) {
- // A B character enables black line detect mode, gap is the line width.
- labelInfo.mediaMode = Options.LabelMediaGapDetectionMode.markSensing;
- labelInfo.labelGapDots = parseInt(rawGapMode.substring(1));
- } else {
- // Otherwise this is the gap length between labels.
- labelInfo.mediaMode = Options.LabelMediaGapDetectionMode.webSensing;
- labelInfo.labelGapDots = parseInt(rawGapMode);
- }
- // A third value is required for black line, ignored for others.
- if (length[2]) {
- labelInfo.labelGapOffsetDots = parseInt(length[2]);
- }
- break;
- }
- case /^S\d\sD\d\d\sR/.test(str): {
- // S4 D08 R112,000 ZB UN # Config settings 2
- const settings2 = str.trim().split(' ');
- const ref = settings2[2].split(',');
- printerInfo.speed = parseInt(settings2[0].substring(1));
- labelInfo.density = parseInt(settings2[1].substring(1));
- labelInfo.xRef = parseInt(ref[0].substring(1));
- labelInfo.yRef = parseInt(ref[1]);
- labelInfo.orientation = settings2[3].substring(1);
- break;
- }
- case /^I\d,.,\d\d\d\sr[YN]/.test(str): {
- // I8,A,001 rY JF WY # Config settings 1
- const settings1 = str.split(' ');
- printerInfo.doubleBuffering = settings1[1][1] === 'Y';
- break;
- }
- case /^HEAD\s\s\s\susage\s=/.test(str): {
- // HEAD usage = 249,392" # Odometer of the head
- const headsplit = str.substring(15).split(' ');
- printerInfo.headDistanceIn = headsplit[headsplit.length - 1];
- break;
- }
- case /^PRINTER\susage\s=/.test(str): {
- // PRINTER usage = 249,392" # Odometer of the printer
- const printsplit = str.substring(15).split(' ');
- printerInfo.printerDistanceIn = printsplit[printsplit.length - 1];
- break;
- }
- case /^Option:/.test(str):
- // Option:D,Ff # Config settings 4
- printerInfo.hardwareOptions = str.substring(7).split(',');
- break;
- case /^Line\sMode/.test(str):
- // Line mode # Printer is in EPL1 mode
- throw new WebZlpError(
- 'Printer is in EPL1 mode, this library does not support EPL1. Reset printer.'
- );
- //
- // Everything else isn't parsed into something interesting.
- // We explicitly parse and handle them to better identify things we don't
- // parse, so we can log that information.
- //
- case /^Page\sMode/.test(str):
- // Page mode # Printer is in EPL2 mode
- // No-op, this is the mode we want in WebZLP
- case /^oE.,/.test(str):
- // oEv,w,x,y,z # Config settings 5
- // Line mode font substitution settings, ignored in WebZLP
- case /^oU.,/.test(str):
- // oUs,t,u # UNKNOWN!
- // Unknown information, only seen on a UPS model so far.
- case /^\d\d\s\d\d\s\d\d\s$/.test(str):
- // 06 10 14 # Config setting 6
- // Not useful information, ignored in WebZLP
- case /^Emem[:\s]/.test(str):
- // Emem:031K,0037K avl # Soft font storage
- // Emem used: 0 # Soft font storage
- case /^Gmem[:\s]/.test(str):
- // Gmem:000K,0037K avl # Graphics storage
- // Gmem used: 0 # Graphics storage
- case /^Fmem[:\s]/.test(str):
- // Fmem:000.0K,060.9K avl # Form storage
- // Fmem used: 0 (bytes) # Form storage
- case /^Available:/.test(str):
- // Available: 130559 # Total memory for Forms, Fonts, or Graphics
- case /^Cover:/.test(str):
- // Cover: T=118, C=129 # (T)reshold and (C)urrent Head Up (open) sensor.
- case /^Image buffer size:/.test(str):
- // Image buffer size:0245K # Image buffer size in use
- break;
- default:
- console.log(
- "WebZLP observed a config line from your printer that was not handled. We'd love it if you could report this bug! Send '" +
- str +
- "' to https://github.com/Cellivar/WebZLP/issues"
- );
- break;
- }
- }
-
- // For any of the called-out sections above see the docs for WebZLP.
-
- if (printerInfo.model == PrinterModel.unknown) {
- // Break the rule of not directly logging errors for this ask.
- console.error(
- `An EPL printer was detected, but WebZLP doesn't know what model it is to communicate with it. Consider submitting an issue to the project at https://github.com/Cellivar/WebZLP/issues to have your printer added so the library can work with it. The information to attach is:`,
- '\nmodel:',
- rawModelId,
- '\nfirmware:',
- printerInfo.firmware,
- '\nconfigLine',
- lines[0],
- '\nAnd include any other details you have about your printer. Thank you!'
- );
- return PrinterOptions.invalid();
- }
-
- // Marshall it into a real data structure as best we can.
- // TODO: Better way to do this?
- const expectedModel = PrinterModelDb.getModelInfo(printerInfo.model);
- const options = new PrinterOptions(printerInfo.serial, expectedModel, printerInfo.firmware);
-
- const rawDarkness = Math.ceil(labelInfo.density * (100 / expectedModel.maxDarkness));
- options.darknessPercent = Math.max(
- 0,
- Math.min(rawDarkness, 100)
- ) as Options.DarknessPercent;
-
- options.speed = new Options.PrintSpeedSettings(
- options.model.fromRawSpeed(printerInfo.speed)
- );
-
- const labelRoundingStep = commOpts.labelDimensionRoundingStep ?? 0;
- if (labelRoundingStep > 0) {
- // Label size should be rounded to the step value by round-tripping the value to an inch
- // then rounding, then back to dots.
- const roundedWidth = this.roundToNearestStep(
- labelInfo.labelWidthDots / options.model.dpi,
- labelRoundingStep
- );
- options.labelWidthDots = roundedWidth * options.model.dpi;
- const roundedHeight = this.roundToNearestStep(
- labelInfo.labelHeightDots / options.model.dpi,
- labelRoundingStep
- );
- options.labelHeightDots = roundedHeight * options.model.dpi;
- } else {
- // No rounding
- options.labelWidthDots = labelInfo.labelWidthDots;
- options.labelHeightDots = labelInfo.labelHeightDots;
- }
-
- // No rounding applied to other offsets, those tend to be stable.
- options.labelGapDots = labelInfo.labelGapDots;
- options.labelLineOffsetDots = labelInfo.labelGapOffsetDots;
-
- options.labelGapDetectMode = labelInfo.mediaMode;
-
- options.labelPrintOriginOffsetDots = { left: labelInfo.xRef, top: labelInfo.yRef };
-
- options.printOrientation =
- labelInfo.orientation === 'T'
- ? Options.PrintOrientation.inverted
- : Options.PrintOrientation.normal;
-
- // Hardware options are listed as various flags.
- // Presence of d or D indicates direct thermal printing, absence indicates transfer.
- if (printerInfo.hardwareOptions.some((o) => o === 'd' || o === 'D')) {
- options.thermalPrintMode = Options.ThermalPrintMode.direct;
- } else {
- options.thermalPrintMode = Options.ThermalPrintMode.transfer;
+ // We make a lot of assumptions about the format of the config output.
+ // Unfortunately EPL-only printers tended to have a LOT of variance on
+ // what they actually put into the config. Firmware versions, especially
+ // shipper-customized versions, can and do omit information.
+ // This method attempts to get what we can out of it.
+
+ // See the docs folder for more information on this format.
+
+ // First line determines firmware version and model number. When splitting
+ // the string by spaces the last element should always be the version and
+ // the rest of the elements are the model number.
+ // UKQ1935HLU V4.29 // Normal LP244
+ // UKQ1935HMU FDX V4.45 // FedEx modified LP2844
+ // UKQ1935H U UPS V4.14 // UPS modified LP2844
+ const header = lines[0].split(' ').filter((i) => i);
+ const firmwareVersion = header.pop() ?? '';
+ const rawModelId = header.join(' ');
+
+ const model = PrinterModelDb.getModel(rawModelId);
+ const expectedModel = PrinterModelDb.getModelInfo(model);
+
+ const printerInfo: {
+ firmware: string,
+ serial: string,
+ serialPort?: string | undefined,
+ speed?: number,
+ doubleBuffering?: boolean,
+ headDistanceIn?: string,
+ printerDistanceIn?: string,
+ hardwareOptions: string[]
+ } = {
+ firmware: firmwareVersion,
+ hardwareOptions: [],
+ serial: 'no_serial_nm'
+ };
+
+ const labelInfo: {
+ labelWidthDots?: number,
+ labelGapDots?: number,
+ labelGapOffsetDots?: number,
+ labelHeightDots?: number,
+ density: number,
+ xRef: number,
+ yRef: number,
+ orientation?: string,
+ mediaMode: Options.LabelMediaGapDetectionMode
+ } = {
+ xRef: 0,
+ yRef: 0,
+ density: (expectedModel.maxDarkness / 2),
+ mediaMode: Options.LabelMediaGapDetectionMode.webSensing
+ };
+
+ // All the rest of these follow some kind of standard pattern for
+ // each value which we can pick up with regex. The cases here are
+ // built out of observed configuration dumps.
+ for (let i = 1; i < lines.length; i++) {
+ const str = lines[i];
+ switch (true) {
+ case /^S\/N.*/.test(str):
+ // S/N: 42A000000000 # Serial number
+ printerInfo.serial = str.substring(5).trim();
+ break;
+ case /^Serial\sport/.test(str):
+ // Serial port:96,N,8,1 # Serial port config
+ printerInfo.serialPort = str.substring(12).trim();
+ break;
+ case /^q\d+\sQ/.test(str): {
+ // q600 Q208,25 # Form width (q) and length (Q), with label gap
+ const settingsForm = str.trim().split(' ');
+ // Label width includes 4 dots of padding. Ish. Maybe.
+ labelInfo.labelWidthDots = parseInt(settingsForm[0].substring(1)) - 4;
+ // Length is fuzzy, depending on the second value this can be
+ // A: The length of the label surface
+ // B: The distance between black line marks
+ // C: The length of the form on continuous media
+ // Format is Qp1,p2[,p3]
+ const length = settingsForm[1].split(',');
+ // p1 is always present and can be treated as the 'label height' consistently.
+ labelInfo.labelHeightDots = parseInt(length[0].substring(1));
+ // p2 value depends on...
+ const rawGapMode = length[1].trim();
+ if (rawGapMode === '0') {
+ // Length of '0' indicates continuous media.
+ labelInfo.mediaMode = Options.LabelMediaGapDetectionMode.continuous;
+ } else if (rawGapMode.startsWith('B')) {
+ // A B character enables black line detect mode, gap is the line width.
+ labelInfo.mediaMode = Options.LabelMediaGapDetectionMode.markSensing;
+ labelInfo.labelGapDots = parseInt(rawGapMode.substring(1));
+ } else {
+ // Otherwise this is the gap length between labels.
+ labelInfo.mediaMode = Options.LabelMediaGapDetectionMode.webSensing;
+ labelInfo.labelGapDots = parseInt(rawGapMode);
+ }
+ // A third value is required for black line, ignored for others.
+ if (length[2]) {
+ labelInfo.labelGapOffsetDots = parseInt(length[2]);
+ }
+ break;
}
-
- // EPL spreads print mode across multiple settings that are mutually exclusive.
- if (printerInfo.hardwareOptions.some((o) => o === 'C')) {
- options.mediaPrintMode = Options.MediaPrintMode.cutter;
+ case /^S\d\sD\d\d\sR/.test(str): {
+ // S4 D08 R112,000 ZB UN # Config settings 2
+ const settings2 = str.trim().split(' ');
+ const ref = settings2[2].split(',');
+ printerInfo.speed = parseInt(settings2[0].substring(1));
+ labelInfo.density = parseInt(settings2[1].substring(1));
+ labelInfo.xRef = parseInt(ref[0].substring(1));
+ labelInfo.yRef = parseInt(ref[1]);
+ labelInfo.orientation = settings2[3].substring(1);
+ break;
}
- if (printerInfo.hardwareOptions.some((o) => o === 'Cp')) {
- options.mediaPrintMode = Options.MediaPrintMode.cutterWaitForCommand;
+ case /^I\d,.,\d\d\d\sr[YN]/.test(str): {
+ // I8,A,001 rY JF WY # Config settings 1
+ const settings1 = str.split(' ');
+ printerInfo.doubleBuffering = settings1[1][1] === 'Y';
+ break;
}
- if (printerInfo.hardwareOptions.some((o) => o === 'P')) {
- options.mediaPrintMode = Options.MediaPrintMode.peel;
+ case /^HEAD\s\s\s\susage\s=/.test(str): {
+ // HEAD usage = 249,392" # Odometer of the head
+ const headSplit = str.substring(15).split(' ');
+ printerInfo.headDistanceIn = headSplit[headSplit.length - 1];
+ break;
}
- if (printerInfo.hardwareOptions.some((o) => o === 'L')) {
- options.mediaPrintMode = Options.MediaPrintMode.peelWithButtonTap;
+ case /^PRINTER\susage\s=/.test(str): {
+ // PRINTER usage = 249,392" # Odometer of the printer
+ const printSplit = str.substring(15).split(' ');
+ printerInfo.printerDistanceIn = printSplit[printSplit.length - 1];
+ break;
}
-
- // TODO: more hardware options:
- // - Form feed button mode (Ff, Fr, Fi)
- // - Figure out what reverse gap sensor mode S means
- // - Figure out how to encode C{num} for cut-after-label-count
-
- // TODO other options:
- // Autosense settings?
- // Character set?
- // Error handling?
- // Continuous media?
- // Black mark printing?
-
- return options;
+ case /^Option:/.test(str):
+ // Option:D,Ff # Config settings 4
+ printerInfo.hardwareOptions = str.substring(7).split(',');
+ break;
+ case /^Line\sMode/.test(str):
+ // Line mode # Printer is in EPL1 mode
+ throw new WebZlpError(
+ 'Printer is in EPL1 mode, this library does not support EPL1. Reset printer.'
+ );
+ //
+ // Everything else isn't parsed into something interesting.
+ // We explicitly parse and handle them to better identify things we don't
+ // parse, so we can log that information.
+ //
+ case /^Page\sMode/.test(str):
+ // Page mode # Printer is in EPL2 mode
+ // No-op, this is the mode we want in WebZLP
+ case /^oE.,/.test(str):
+ // oEv,w,x,y,z # Config settings 5
+ // Line mode font substitution settings, ignored in WebZLP
+ case /^oU.,/.test(str):
+ // oUs,t,u # UNKNOWN!
+ // Unknown information, only seen on a UPS model so far.
+ case /^\d\d\s\d\d\s\d\d\s$/.test(str):
+ // 06 10 14 # Config setting 6
+ // Not useful information, ignored in WebZLP
+ case /^Emem[:\s]/.test(str):
+ // Emem:031K,0037K avl # Soft font storage
+ // Emem used: 0 # Soft font storage
+ case /^Gmem[:\s]/.test(str):
+ // Gmem:000K,0037K avl # Graphics storage
+ // Gmem used: 0 # Graphics storage
+ case /^Fmem[:\s]/.test(str):
+ // Fmem:000.0K,060.9K avl # Form storage
+ // Fmem used: 0 (bytes) # Form storage
+ case /^Available:/.test(str):
+ // Available: 130559 # Total memory for Forms, Fonts, or Graphics
+ case /^Cover:/.test(str):
+ // Cover: T=118, C=129 # (T)reshold and (C)urrent Head Up (open) sensor.
+ case /^Image buffer size:/.test(str):
+ // Image buffer size:0245K # Image buffer size in use
+ break;
+ default:
+ console.log(
+ "WebZLP observed a config line from your printer that was not handled. We'd love it if you could report this bug! Send '" +
+ str +
+ "' to https://github.com/Cellivar/WebZLP/issues"
+ );
+ break;
+ }
}
- private setPrintDirectionCommand(
- cmd: Commands.SetPrintDirectionCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const dir = cmd.upsideDown ? 'T' : 'B';
- return cmdSet.encodeCommand(`Z${dir}`);
+ // For any of the called-out sections above see the docs for WebZLP.
+
+ if (model === PrinterModel.unknown) {
+ // Break the rule of not directly logging errors for this ask.
+ console.error(
+ `An EPL printer was detected, but WebZLP doesn't know what model it is to communicate with it. Consider submitting an issue to the project at https://github.com/Cellivar/WebZLP/issues to have your printer added so the library can work with it. The information to attach is:`,
+ '\nmodel:',
+ rawModelId,
+ '\nfirmware:',
+ printerInfo.firmware,
+ '\nconfigLine',
+ lines[0],
+ '\nAnd include any other details you have about your printer. Thank you!'
+ );
+ return PrinterOptions.invalid;
}
- private setDarknessCommand(
- cmd: Commands.SetDarknessCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const dark = Math.trunc(cmd.darknessSetting);
- return cmdSet.encodeCommand(`D${dark}`);
+ // Marshall it into a real data structure as best we can.
+ // TODO: Better way to do this?
+ const options = new PrinterOptions(printerInfo.serial, expectedModel, printerInfo.firmware);
+
+ const darkPercent = Math.ceil(labelInfo.density * (100 / expectedModel.maxDarkness));
+ options.darknessPercent = clampToRange(darkPercent, 0, expectedModel.maxDarkness) as Options.DarknessPercent;
+
+ options.speed = new Options.PrintSpeedSettings(
+ options.model.fromRawSpeed(printerInfo.speed)
+ );
+ const rounding = mediaOptions.labelDimensionRoundingStep;
+ if (rounding > 0 && labelInfo.labelWidthDots !== undefined && labelInfo.labelHeightDots !== undefined) {
+ // Label size should be rounded to the step value by round-tripping the value to an inch
+ // then rounding, then back to dots.
+ const roundedWidth = this.roundToNearestStep(
+ labelInfo.labelWidthDots / options.model.dpi,
+ rounding
+ );
+ options.labelWidthDots = roundedWidth * options.model.dpi;
+ const roundedHeight = this.roundToNearestStep(
+ labelInfo.labelHeightDots / options.model.dpi,
+ rounding
+ );
+ options.labelHeightDots = roundedHeight * options.model.dpi;
+ } else {
+ // No rounding
+ options.labelWidthDots = labelInfo.labelWidthDots ?? 100;
+ options.labelHeightDots = labelInfo.labelHeightDots ?? 100;
}
- private setPrintSpeedCommand(
- cmd: Commands.SetPrintSpeedCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- // Validation should have happened on setup, printer will just reject
- // invalid speeds.
- // EPL has no separate media slew speed setting.
- return cmdSet.encodeCommand(`S${cmd.speedVal}`);
- }
+ // No rounding applied to other offsets, those tend to be stable.
+ options.labelGapDots = labelInfo.labelGapDots ?? 0;
+ options.labelLineOffsetDots = labelInfo.labelGapOffsetDots ?? 0;
- private setLabelDimensionsCommand(
- cmd: Commands.SetLabelDimensionsCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const width = Math.trunc(cmd.widthInDots);
- const widthCmd = cmdSet.encodeCommand(`q${width}`);
- if (cmd.setsHeight) {
- const height = Math.trunc(cmd.heightInDots);
- const gap = Math.trunc(cmd.gapLengthInDots);
- const heightCmd = cmdSet.encodeCommand(`Q${height},${gap}`);
- return cmdSet.combineCommands(widthCmd, heightCmd);
- }
- return widthCmd;
- }
+ options.labelGapDetectMode = labelInfo.mediaMode;
- private setLabelHomeCommand(
- cmd: Commands.SetLabelHomeCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- return this.modifyOffset(
- new Commands.OffsetCommand(cmd.xOffset, cmd.yOffset, true),
- outDoc,
- cmdSet
- );
- }
+ options.labelPrintOriginOffsetDots = { left: labelInfo.xRef, top: labelInfo.yRef };
- private setLabelPrintOriginOffsetCommand(
- cmd: Commands.SetLabelPrintOriginOffsetCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const xOffset = Math.trunc(cmd.xOffset);
- const yOffset = Math.trunc(cmd.yOffset);
- return cmdSet.encodeCommand(`R${xOffset},${yOffset}`);
- }
+ options.printOrientation =
+ labelInfo.orientation === 'T'
+ ? Options.PrintOrientation.inverted
+ : Options.PrintOrientation.normal;
- private setLabelToContinuousMediaCommand(
- cmd: Commands.SetLabelToContinuousMediaCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- // EPL seems to not have a static label length? All labels are variable?
- // Needs testing.
- const length = Math.trunc(cmd.labelLengthInDots);
- return cmdSet.encodeCommand(`Q${length},0`);
+ // Hardware options are listed as various flags.
+ // Presence of d or D indicates direct thermal printing, absence indicates transfer.
+ if (printerInfo.hardwareOptions.some((o) => o === 'd' || o === 'D')) {
+ options.thermalPrintMode = Options.ThermalPrintMode.direct;
+ } else {
+ options.thermalPrintMode = Options.ThermalPrintMode.transfer;
}
- private setLabelToWebGapMediaCommand(
- cmd: Commands.SetLabelToWebGapMediaCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const length = Math.trunc(cmd.labelLengthInDots);
- const gap = Math.trunc(cmd.labelGapInDots);
- return cmdSet.encodeCommand(`Q${length},${gap}`);
+ // EPL spreads print mode across multiple settings that are mutually exclusive.
+ if (printerInfo.hardwareOptions.some((o) => o === 'C')) {
+ options.mediaPrintMode = Options.MediaPrintMode.cutter;
}
-
- private setLabelToMarkMediaCommand(
- cmd: Commands.SetLabelToMarkMediaCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const length = Math.trunc(cmd.labelLengthInDots);
- const lineLength = Math.trunc(cmd.blackLineThicknessInDots);
- const lineOffset = Math.trunc(cmd.blackLineOffset);
- return cmdSet.encodeCommand(`Q${length},B${lineLength},${lineOffset}`);
+ if (printerInfo.hardwareOptions.some((o) => o === 'Cp')) {
+ options.mediaPrintMode = Options.MediaPrintMode.cutterWaitForCommand;
}
-
- private printCommand(
- cmd: Commands.PrintCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const total = Math.trunc(cmd.count);
- const dup = Math.trunc(cmd.additionalDuplicateOfEach);
- return cmdSet.encodeCommand(`P${total},${dup}`);
+ if (printerInfo.hardwareOptions.some((o) => o === 'P')) {
+ options.mediaPrintMode = Options.MediaPrintMode.peel;
}
-
- private addImageCommand(
- cmd: Commands.AddImageCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- if (cmd?.bitmap == null) {
- return cmdSet.noop;
- }
-
- // EPL only supports raw binary, get that.
- const bitmap = cmd.bitmap;
- const buffer = bitmap.toBinaryGRF();
-
- // Add the text command prefix to the buffer data
- const parameters = [
- 'GW' + Math.trunc(outDoc.horizontalOffset + bitmap.boundingBox.paddingLeft),
- Math.trunc(outDoc.verticalOffset + bitmap.boundingBox.paddingTop),
- bitmap.bytesPerRow,
- bitmap.height
- ];
- // Bump the offset according to the image being added.
- outDoc.verticalOffset += bitmap.boundingBox.height;
- const rawCmd = cmdSet.encodeCommand(parameters.join(',') + ',', false);
- return cmdSet.combineCommands(
- rawCmd,
- cmdSet.combineCommands(buffer, cmdSet.encodeCommand(''))
- );
+ if (printerInfo.hardwareOptions.some((o) => o === 'L')) {
+ options.mediaPrintMode = Options.MediaPrintMode.peelWithButtonTap;
}
- private addLineCommand(
- cmd: Commands.AddLineCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const length = Math.trunc(cmd.lengthInDots) || 0;
- const height = Math.trunc(cmd.heightInDots) || 0;
- let drawMode = 'LO';
- switch (cmd.color) {
- case Commands.DrawColor.black:
- drawMode = 'LO';
- break;
- case Commands.DrawColor.white:
- drawMode = 'LW';
- break;
- }
-
- return cmdSet.encodeCommand(
- `${drawMode}${outDoc.horizontalOffset},${outDoc.verticalOffset},${length},${height}`
- );
+ // TODO: more hardware options:
+ // - Form feed button mode (Ff, Fr, Fi)
+ // - Figure out what reverse gap sensor mode S means
+ // - Figure out how to encode C{num} for cut-after-label-count
+
+ // TODO other options:
+ // Autosense settings?
+ // Character set?
+ // Error handling?
+ // Continuous media?
+ // Black mark printing?
+
+ return options;
+ }
+
+ private setPrintDirectionCommand(
+ upsideDown: boolean
+ ): Uint8Array {
+ const dir = upsideDown ? 'T' : 'B';
+ return this.encodeCommand(`Z${dir}`);
+ }
+
+ private setDarknessCommand(
+ darkness: number
+ ): Uint8Array {
+ const dark = Math.trunc(darkness);
+ return this.encodeCommand(`D${dark}`);
+ }
+
+ private setPrintSpeedCommand(
+ speed: number
+ ): Uint8Array {
+ // Validation should have happened on setup, printer will just reject
+ // invalid speeds.
+ return this.encodeCommand(`S${speed}`);
+ }
+
+ private setLabelDimensionsCommand(
+ cmd: Commands.SetLabelDimensionsCommand,
+ ): Uint8Array {
+ const width = Math.trunc(cmd.widthInDots);
+ const widthCmd = this.encodeCommand(`q${width}`);
+ if (cmd.setsHeight && cmd.heightInDots !== undefined && cmd.gapLengthInDots !== undefined) {
+ const height = Math.trunc(cmd.heightInDots);
+ const gap = Math.trunc(cmd.gapLengthInDots);
+ const heightCmd = this.encodeCommand(`Q${height},${gap}`);
+ return this.combineCommands(widthCmd, heightCmd);
}
-
- private addBoxCommand(
- cmd: Commands.AddBoxCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: EplPrinterCommandSet
- ): Uint8Array {
- const length = Math.trunc(cmd.lengthInDots) || 0;
- const height = Math.trunc(cmd.heightInDots) || 0;
- const thickness = Math.trunc(cmd.thickness) || 0;
-
- return cmdSet.encodeCommand(
- `X${outDoc.horizontalOffset},${outDoc.verticalOffset},${thickness},${length},${height}`
- );
+ return widthCmd;
+ }
+
+ private setLabelHomeCommand(
+ cmd: Commands.SetLabelHomeCommand,
+ outDoc: TranspiledDocumentState,
+ cmdSet: EplPrinterCommandSet
+ ): Uint8Array {
+ return this.modifyOffset(
+ new Commands.OffsetCommand(cmd.xOffset, cmd.yOffset, true),
+ outDoc,
+ cmdSet
+ );
+ }
+
+ private setLabelPrintOriginOffsetCommand(
+ cmd: Commands.SetLabelPrintOriginOffsetCommand,
+ ): Uint8Array {
+ const xOffset = Math.trunc(cmd.xOffset);
+ const yOffset = Math.trunc(cmd.yOffset);
+ return this.encodeCommand(`R${xOffset},${yOffset}`);
+ }
+
+ private setLabelToContinuousMediaCommand(
+ cmd: Commands.SetLabelToContinuousMediaCommand,
+ ): Uint8Array {
+ // EPL seems to not have a static label length? All labels are variable?
+ // Needs testing.
+ const length = Math.trunc(cmd.labelLengthInDots);
+ return this.encodeCommand(`Q${length},0`);
+ }
+
+ private setLabelToWebGapMediaCommand(
+ cmd: Commands.SetLabelToWebGapMediaCommand,
+ ): Uint8Array {
+ const length = Math.trunc(cmd.labelLengthInDots);
+ const gap = Math.trunc(cmd.labelGapInDots);
+ return this.encodeCommand(`Q${length},${gap}`);
+ }
+
+ private setLabelToMarkMediaCommand(
+ cmd: Commands.SetLabelToMarkMediaCommand,
+ ): Uint8Array {
+ const length = Math.trunc(cmd.labelLengthInDots);
+ const lineLength = Math.trunc(cmd.blackLineThicknessInDots);
+ const lineOffset = Math.trunc(cmd.blackLineOffset);
+ return this.encodeCommand(`Q${length},B${lineLength},${lineOffset}`);
+ }
+
+ private printCommand(
+ cmd: Commands.PrintCommand,
+ ): Uint8Array {
+ const total = Math.trunc(cmd.count);
+ const dup = Math.trunc(cmd.additionalDuplicateOfEach);
+ return this.encodeCommand(`P${total},${dup}`);
+ }
+
+ private addImageCommand(
+ cmd: Commands.AddImageCommand,
+ outDoc: TranspiledDocumentState,
+ ): Uint8Array {
+ // EPL only supports raw binary, get that.
+ const bitmap = cmd.bitmap;
+ const buffer = bitmap.toBinaryGRF();
+
+ // Add the text command prefix to the buffer data
+ const parameters = [
+ 'GW' + Math.trunc(outDoc.horizontalOffset + bitmap.boundingBox.paddingLeft),
+ Math.trunc(outDoc.verticalOffset + bitmap.boundingBox.paddingTop),
+ bitmap.bytesPerRow,
+ bitmap.height
+ ];
+ // Bump the offset according to the image being added.
+ outDoc.verticalOffset += bitmap.boundingBox.height;
+ const rawCmd = this.encodeCommand(parameters.join(',') + ',', false);
+ return this.combineCommands(
+ rawCmd,
+ this.combineCommands(buffer, this.encodeCommand(''))
+ );
+ }
+
+ private addLineCommand(
+ cmd: Commands.AddLineCommand,
+ outDoc: TranspiledDocumentState,
+ ): Uint8Array {
+ const length = Math.trunc(cmd.lengthInDots) || 0;
+ const height = Math.trunc(cmd.heightInDots) || 0;
+ let drawMode = 'LO';
+ switch (cmd.color) {
+ case Commands.DrawColor.black:
+ drawMode = 'LO';
+ break;
+ case Commands.DrawColor.white:
+ drawMode = 'LW';
+ break;
}
+
+ return this.encodeCommand(
+ `${drawMode}${outDoc.horizontalOffset},${outDoc.verticalOffset},${length},${height}`
+ );
+ }
+
+ private addBoxCommand(
+ cmd: Commands.AddBoxCommand,
+ outDoc: TranspiledDocumentState,
+ ): Uint8Array {
+ const length = Math.trunc(cmd.lengthInDots) || 0;
+ const height = Math.trunc(cmd.heightInDots) || 0;
+ const thickness = Math.trunc(cmd.thickness) || 0;
+
+ return this.encodeCommand(
+ `X${outDoc.horizontalOffset},${outDoc.verticalOffset},${thickness},${length},${height}`
+ );
+ }
}
diff --git a/src/Printers/Languages/PrinterCommandSet.ts b/src/Printers/Languages/PrinterCommandSet.ts
index 376badb..63d6856 100644
--- a/src/Printers/Languages/PrinterCommandSet.ts
+++ b/src/Printers/Languages/PrinterCommandSet.ts
@@ -1,297 +1,324 @@
-import { CompiledDocument, IDocument } from '../../Documents/Document.js';
+import { CompiledDocument, type IDocument } from '../../Documents/Document.js';
import { WebZlpError } from '../../WebZlpError.js';
-import { PrinterCommandLanguage, PrinterOptions } from '../Configuration/PrinterOptions.js';
+import { PrinterCommandLanguage, PrinterOptions, type IPrinterLabelMediaOptions } from '../Configuration/PrinterOptions.js';
import * as Commands from '../../Documents/Commands.js';
-import { PrinterCommunicationOptions } from '../PrinterCommunicationOptions.js';
-export type TranspileCommandDelegate = (
- cmd: Commands.IPrinterCommand,
- formDoc: TranspilationFormMetadata,
- commandSet: PrinterCommandSet
-) => Uint8Array;
+/** A manifest for a custom extended printer command. */
+export interface IPrinterExtendedCommandMapping {
+ extendedTypeSymbol: symbol,
+ delegate: TranspileCommandDelegate,
+}
+
+export type TranspileCommandDelegate = (
+ cmd: Commands.IPrinterCommand,
+ formDoc: TranspiledDocumentState,
+ commandSet: PrinterCommandSet
+) => TOutput;
type RawCommandForm = { commands: Array; withinForm: boolean };
export abstract class PrinterCommandSet {
- /** Encode a raw string command into a Uint8Array according to the command language rules. */
- public abstract encodeCommand(str?: string, withNewline?: boolean): Uint8Array;
-
- private readonly _noop = new Uint8Array();
- /** Get an empty command to be used as a no-op. */
- protected get noop() {
- return this._noop;
- }
-
- /** Gets the command to start a new form. */
- protected abstract get formStartCommand(): Uint8Array;
- /** Gets the command to end a form. */
- protected abstract get formEndCommand(): Uint8Array;
- /** Gets the command language this command set implements */
- abstract get commandLanguage(): PrinterCommandLanguage;
-
- protected abstract transpileCommandMap: Map<
- symbol | Commands.CommandType,
- TranspileCommandDelegate
- >;
-
- /** Transpile a command to its native command equivalent. */
- protected transpileCommand(
- command: Commands.IPrinterCommand,
- formMetadata: TranspilationFormMetadata
- ) {
- let lookup: symbol | Commands.CommandType;
- if (
- command.type === Commands.CommandType.CommandCustomSpecificCommand ||
- command.type === Commands.CommandType.CommandLanguageSpecificCommand
- ) {
- lookup = (command as Commands.IPrinterExtendedCommand).typeExtended;
- } else {
- lookup = command.type;
- }
-
- if (!lookup) {
- throw new DocumentValidationError(
- `Command '${command.constructor.name}' did not have a valid lookup element. If you're trying to implement a custom command check the documentation for correct formatting.`
- );
- }
-
- const func = this.transpileCommandMap.get(lookup);
- if (func === undefined) {
- throw new DocumentValidationError(
- // eslint-disable-next-line prettier/prettier
- `Unknown command '${command.constructor.name}' was not found in the command map for ${PrinterCommandLanguage[this.commandLanguage]} command language. If you're trying to implement a custom command check the documentation for correctly adding mappings.`
- );
- }
-
- return func(command, formMetadata, this);
- }
-
- public transpileDoc(doc: IDocument): Readonly {
- const validationErrors = [];
- const { forms, effects } = this.splitCommandsByFormInclusion(
- doc.commands,
- doc.commandReorderBehavior
- );
-
- const commandsWithMaybeErrors = forms.flatMap((form) => this.transpileForm(form));
- const errs = commandsWithMaybeErrors.filter(
- (e): e is DocumentValidationError => !(e instanceof Uint8Array)
- );
- if (errs.length > 0) {
- throw new DocumentValidationError(
- 'One or more validation errors occurred transpiling the document.',
- validationErrors
- );
- }
-
- // Combine the separate individual documents into a single command array.
- const buffer = commandsWithMaybeErrors.reduce((accumulator, cmd) => {
- if (!(cmd instanceof Uint8Array)) {
- throw new DocumentValidationError(
- 'Document validation error present after checking for one!?!? Error in WebZLP!',
- [cmd]
- );
- }
- return this.combineCommands(accumulator as Uint8Array, cmd);
- // We start with an explicit newline, to avoid possible previous commands partially sent
- }, this.encodeCommand());
-
- const out = new CompiledDocument(this.commandLanguage, effects, buffer);
- return Object.freeze(out);
+ /** Encode a raw string command into a Uint8Array according to the command language rules. */
+ public abstract encodeCommand(str?: string, withNewline?: boolean): Uint8Array;
+
+ private readonly _noop = new Uint8Array();
+ /** Get an empty command to be used as a no-op. */
+ protected get noop() {
+ return this._noop;
+ }
+
+ /** Gets the command to start a new form. */
+ protected abstract get formStartCommand(): Uint8Array;
+ /** Gets the command to end a form. */
+ protected abstract get formEndCommand(): Uint8Array;
+ /** Gets the command language this command set implements */
+ abstract get commandLanguage(): PrinterCommandLanguage;
+
+ protected extendedCommandMap = new Map>;
+
+ protected constructor(
+ public readonly implementedLanguage: PrinterCommandLanguage,
+ extendedCommands: Array> = []
+ ) {
+ extendedCommands.forEach(c => this.extendedCommandMap.set(c.extendedTypeSymbol, c.delegate));
+ }
+
+ /** Transpile a command to its native command equivalent. */
+ public abstract transpileCommand(
+ cmd: Commands.IPrinterCommand,
+ docState: TranspiledDocumentState
+ ): Uint8Array | TranspileDocumentError;
+
+ // protected transpileCommand(
+ // command: Commands.IPrinterCommand,
+ // formMetadata: TranspilationFormMetadata
+ // ) {
+ // let lookup: symbol | Commands.CommandType;
+ // if (
+ // command.type === Commands.CommandType.CommandCustomSpecificCommand ||
+ // command.type === Commands.CommandType.CommandLanguageSpecificCommand
+ // ) {
+ // lookup = (command as Commands.IPrinterExtendedCommand).typeExtended;
+ // } else {
+ // lookup = command.type;
+ // }
+
+ // if (!lookup) {
+ // throw new TranspileDocumentError(
+ // `Command '${command.constructor.name}' did not have a valid lookup element. If you're trying to implement a custom command check the documentation for correct formatting.`
+ // );
+ // }
+
+ // const func = this.transpileCommandMap.get(lookup);
+ // if (func === undefined) {
+ // throw new TranspileDocumentError(
+ // `Unknown command '${command.constructor.name}' was not found in the command map for ${PrinterCommandLanguage[this.commandLanguage]} command language. If you're trying to implement a custom command check the documentation for correctly adding mappings.`
+ // );
+ // }
+
+ // return func(command, formMetadata, this);
+ // }
+
+ protected extendedCommandHandler(
+ cmd: Commands.IPrinterCommand,
+ docState: TranspiledDocumentState
+ ) {
+ const lookup = (cmd as Commands.IPrinterExtendedCommand).typeExtended;
+ if (!lookup) {
+ throw new TranspileDocumentError(
+ `Command '${cmd.constructor.name}' did not have a value for typeExtended. If you're trying to implement a custom command check the documentation.`
+ )
}
- private transpileForm({
- commands,
- withinForm
- }: RawCommandForm): Array {
- const formMetadata = new TranspilationFormMetadata();
- const transpiledCommands = commands.map((cmd) => this.transpileCommand(cmd, formMetadata));
- if (withinForm) {
- transpiledCommands.unshift(this.formStartCommand);
- transpiledCommands.push(this.formEndCommand);
- }
-
- return transpiledCommands;
- }
+ const cmdHandler = this.extendedCommandMap.get(lookup);
- private splitCommandsByFormInclusion(
- commands: ReadonlyArray,
- reorderBehavior: Commands.CommandReorderBehavior
- ): { forms: Array; effects: Commands.PrinterCommandEffectFlags } {
- const forms: Array = [];
- const nonForms: Array = [];
- let effects = Commands.PrinterCommandEffectFlags.none;
- for (const command of commands) {
- effects |= command.printerEffectFlags;
- if (
- this.isCommandNonFormCommand(command) &&
- reorderBehavior === Commands.CommandReorderBehavior.nonFormCommandsAfterForms
- ) {
- nonForms.push({ commands: [command], withinForm: false });
- continue;
- }
-
- if (command.type == Commands.CommandType.NewLabelCommand) {
- // Since form bounding is implicit this is our indicator to break
- // between separate forms to be printed separately.
- forms.push({ commands: [], withinForm: true });
- continue;
- }
-
- // Anything else just gets tossed onto the stack of the current form, if it exists.
- if (forms.at(-1) === undefined) {
- forms.push({ commands: [], withinForm: true });
- }
- forms.at(-1).commands.push(command);
- }
-
- // TODO: If the day arises we need to configure non-form commands _before_ the form
- // this will need to be made more clever.
- return { forms: forms.concat(nonForms), effects };
+ if (cmdHandler === undefined) {
+ throw new TranspileDocumentError(
+ `Unknown command '${cmd.constructor.name}' was not found in the command map for ${this.commandLanguage} command language. If you're trying to implement a custom command check the documentation for correctly adding mappings.`
+ );
}
-
- /** List of commands which must not appear within a form, according to this language's rules */
- protected abstract nonFormCommands: Array;
-
- private isCommandNonFormCommand(command: Commands.IPrinterCommand) {
- let id: symbol | Commands.CommandType;
- if (
- command.type === Commands.CommandType.CommandCustomSpecificCommand ||
- command.type === Commands.CommandType.CommandLanguageSpecificCommand
- ) {
- id = (command as Commands.IPrinterExtendedCommand).typeExtended;
- } else {
- id = command.type;
- }
-
- return this.nonFormCommands.includes(id);
+ return cmdHandler(cmd, docState, this);
+ }
+
+ public transpileDoc(doc: IDocument): Readonly {
+ const validationErrors: TranspileDocumentError[] = [];
+ const { forms, effects } = this.splitCommandsByFormInclusion(
+ doc.commands,
+ doc.commandReorderBehavior
+ );
+
+ const commandsWithMaybeErrors = forms.flatMap((form) => this.transpileForm(form));
+ const errs = commandsWithMaybeErrors.filter(
+ (e): e is TranspileDocumentError => !(e instanceof Uint8Array)
+ );
+ if (errs.length > 0) {
+ throw new TranspileDocumentError(
+ 'One or more validation errors occurred transpiling the document.',
+ validationErrors
+ );
}
- protected unprocessedCommand(cmd: Commands.IPrinterCommand): Uint8Array {
- throw new DocumentValidationError(
- `Unhandled meta-command '${cmd.constructor.name}' was not preprocessed. This is a bug in WebZLP, please submit an issue.`
+ // Combine the separate individual documents into a single command array.
+ const buffer = commandsWithMaybeErrors.reduce((accumulator, cmd) => {
+ if (!(cmd instanceof Uint8Array)) {
+ throw new TranspileDocumentError(
+ 'Document validation error present after checking for one!?!? Error in WebZLP!',
+ [cmd]
);
+ }
+ return this.combineCommands(accumulator as Uint8Array, cmd);
+ // We start with an explicit newline, to avoid possible previous commands partially sent
+ }, this.encodeCommand());
+
+ const out = new CompiledDocument(this.commandLanguage, effects, buffer);
+ return Object.freeze(out);
+ }
+
+ private transpileForm({
+ commands,
+ withinForm
+ }: RawCommandForm): Array {
+ const formMetadata = new TranspiledDocumentState();
+ const transpiledCommands = commands.map((cmd) => this.transpileCommand(cmd, formMetadata));
+ if (withinForm) {
+ transpiledCommands.unshift(this.formStartCommand);
+ transpiledCommands.push(this.formEndCommand);
}
- /**
- * Round a raw value to the nearest step.
- */
- protected roundToNearestStep(value: number, step: number): number {
- const inverse = 1.0 / step;
- return Math.round(value * inverse) / inverse;
- }
-
- /** Strip a string of invalid characters for a command. */
- protected cleanString(str: string) {
- return str
- .replace(/\\/gi, '\\\\')
- .replace(/"/gi, '\\"')
- .replace(/[\r\n]+/gi, ' ');
+ return transpiledCommands;
+ }
+
+ private splitCommandsByFormInclusion(
+ commands: ReadonlyArray,
+ reorderBehavior: Commands.CommandReorderBehavior
+ ): { forms: Array; effects: Commands.PrinterCommandEffectFlags } {
+ const forms: Array = [];
+ const nonForms: Array = [];
+ let effects = Commands.PrinterCommandEffectFlags.none;
+ for (const command of commands) {
+ effects |= command.printerEffectFlags;
+ if (
+ this.isCommandNonFormCommand(command) &&
+ reorderBehavior === Commands.CommandReorderBehavior.nonFormCommandsAfterForms
+ ) {
+ nonForms.push({ commands: [command], withinForm: false });
+ continue;
+ }
+
+ if (command.type === 'NewLabelCommand') {
+ // Since form bounding is implicit this is our indicator to break
+ // between separate forms to be printed separately.
+ forms.push({ commands: [], withinForm: true });
+ continue;
+ }
+
+ // Anything else just gets tossed onto the stack of the current form, if it exists.
+ const lastForm = forms.at(-1);
+ if (lastForm === undefined) {
+ forms.push({ commands: [command], withinForm: true });
+ } else {
+ lastForm.commands.push(command);
+ }
}
- /** Apply an offset command to a document. */
- protected modifyOffset(
- cmd: Commands.OffsetCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: PrinterCommandSet
- ) {
- const newHoriz = cmd.absolute ? cmd.horizontal : outDoc.horizontalOffset + cmd.horizontal;
- outDoc.horizontalOffset = newHoriz < 0 ? 0 : newHoriz;
- if (cmd.vertical) {
- const newVert = cmd.absolute ? cmd.vertical : outDoc.verticalOffset + cmd.vertical;
- outDoc.verticalOffset = newVert < 0 ? 0 : newVert;
- }
- return cmdSet.noop;
- }
-
- /** Combine two commands into one command array. */
- protected combineCommands(cmd1: Uint8Array, cmd2: Uint8Array) {
- const merged = new Uint8Array(cmd1.length + cmd2.length);
- merged.set(cmd1);
- merged.set(cmd2, cmd1.length);
- return merged;
- }
-
- /** Clamp a number to a given range of values. */
- protected clampToRange(value: number, min: number, max: number) {
- return Math.min(Math.max(value, min), max);
+ // TODO: If the day arises we need to configure non-form commands _before_ the form
+ // this will need to be made more clever.
+ return { forms: forms.concat(nonForms), effects };
+ }
+
+ /** List of commands which must not appear within a form, according to this language's rules */
+ protected abstract nonFormCommands: Array;
+
+ private isCommandNonFormCommand(command: Commands.IPrinterCommand) {
+ return this.nonFormCommands.includes(
+ command.type === 'CustomCommand'
+ ? (command as Commands.IPrinterExtendedCommand).typeExtended
+ : command.type
+ );
+ }
+
+ protected unprocessedCommand(cmd: Commands.IPrinterCommand): Uint8Array {
+ throw new TranspileDocumentError(
+ `Unhandled meta-command '${cmd.constructor.name}' was not preprocessed. This is a bug in WebZLP, please submit an issue.`
+ );
+ }
+
+ /**
+ * Round a raw value to the nearest step.
+ */
+ protected roundToNearestStep(value: number, step: number): number {
+ const inverse = 1.0 / step;
+ return Math.round(value * inverse) / inverse;
+ }
+
+ /** Strip a string of invalid characters for a command. */
+ protected cleanString(str: string) {
+ return str
+ .replace(/\\/gi, '\\\\')
+ .replace(/"/gi, '\\"')
+ .replace(/[\r\n]+/gi, ' ');
+ }
+
+ /** Apply an offset command to a document. */
+ protected modifyOffset(
+ cmd: Commands.OffsetCommand,
+ outDoc: TranspiledDocumentState,
+ cmdSet: PrinterCommandSet
+ ) {
+ const newHoriz = cmd.absolute ? cmd.horizontal : outDoc.horizontalOffset + cmd.horizontal;
+ outDoc.horizontalOffset = newHoriz < 0 ? 0 : newHoriz;
+ if (cmd.vertical) {
+ const newVert = cmd.absolute ? cmd.vertical : outDoc.verticalOffset + cmd.vertical;
+ outDoc.verticalOffset = newVert < 0 ? 0 : newVert;
}
+ return cmdSet.noop;
+ }
+
+ /** Combine two commands into one command array. */
+ protected combineCommands(cmd1: Uint8Array, cmd2: Uint8Array) {
+ const merged = new Uint8Array(cmd1.length + cmd2.length);
+ merged.set(cmd1);
+ merged.set(cmd2, cmd1.length);
+ return merged;
+ }
+
+ /** Parse the response of a configuration inqury in the command set language. */
+ abstract parseConfigurationResponse(
+ rawText: string,
+ mediaOptions: IPrinterLabelMediaOptions
+ ): PrinterOptions;
+}
- /** Parse the response of a configuration inqury in the command set language. */
- abstract parseConfigurationResponse(
- rawText: string,
- commOpts: PrinterCommunicationOptions
- ): PrinterOptions;
+export function exhaustiveMatchGuard(_: never): never {
+ throw new Error('Invalid case received!' + _);
}
/** How a command should be wrapped into a form, if at all */
export enum CommandFormInclusionMode {
- /** Command can appear in a shared form with other commands. */
- sharedForm = 0,
- /** Command should not be wrapped in a form at all. */
- noForm
+ /** Command can appear in a shared form with other commands. */
+ sharedForm = 0,
+ /** Command should not be wrapped in a form at all. */
+ noForm
}
export class TranspilationFormList {
- private _documents: Array = [new TranspilationFormMetadata()];
- public get documents(): ReadonlyArray {
- return this._documents;
- }
-
- private activeDocumentIdx = 0;
- public get currentDocument() {
- return this._documents[this.activeDocumentIdx];
- }
-
- public addNewDocument() {
- this._documents.push(new TranspilationFormMetadata());
- this.activeDocumentIdx = this._documents.length - 1;
- }
+ private _documents: Array = [new TranspiledDocumentState()];
+ public get documents(): ReadonlyArray {
+ return this._documents;
+ }
+
+ private activeDocumentIdx = 0;
+ public get currentDocument() {
+ return this._documents[this.activeDocumentIdx];
+ }
+
+ public addNewDocument() {
+ this._documents.push(new TranspiledDocumentState());
+ this.activeDocumentIdx = this._documents.length - 1;
+ }
}
/** Class for storing in-progress document generation information */
-export class TranspilationFormMetadata {
- horizontalOffset = 0;
- verticalOffset = 0;
- lineSpacingDots = 5;
-
- dpi: number;
-
- commandEffectFlags = Commands.PrinterCommandEffectFlags.none;
+export class TranspiledDocumentState {
+ horizontalOffset = 0;
+ verticalOffset = 0;
+ lineSpacingDots = 5;
- rawCmdBuffer: Array = [];
+ commandEffectFlags = Commands.PrinterCommandEffectFlags.none;
- /** Add a raw command to the internal buffer. */
- addRawCommand(array: Uint8Array) {
- if (array && array.length > 0) {
- this.rawCmdBuffer.push(array);
- }
- }
+ rawCmdBuffer: Array = [];
- /**
- * Gets a single buffer of the internal command set.
- */
- get combinedBuffer(): Uint8Array {
- const bufferLen = this.rawCmdBuffer.reduce((sum, arr) => sum + arr.byteLength, 0);
- return this.rawCmdBuffer.reduce(
- (accumulator, arr) => {
- accumulator.buffer.set(arr, accumulator.offset);
- return { ...accumulator, offset: arr.byteLength + accumulator.offset };
- },
- { buffer: new Uint8Array(bufferLen), offset: 0 }
- ).buffer;
+ /** Add a raw command to the internal buffer. */
+ addRawCommand(array: Uint8Array) {
+ if (array && array.length > 0) {
+ this.rawCmdBuffer.push(array);
}
+ }
+
+ /**
+ * Gets a single buffer of the internal command set.
+ */
+ get combinedBuffer(): Uint8Array {
+ const bufferLen = this.rawCmdBuffer.reduce((sum, arr) => sum + arr.byteLength, 0);
+ return this.rawCmdBuffer.reduce(
+ (accumulator, arr) => {
+ accumulator.buffer.set(arr, accumulator.offset);
+ return { ...accumulator, offset: arr.byteLength + accumulator.offset };
+ },
+ { buffer: new Uint8Array(bufferLen), offset: 0 }
+ ).buffer;
+ }
}
-/** Represents an error when validating a document against a printer's capabilties. */
-export class DocumentValidationError extends WebZlpError {
- private _innerErrors: DocumentValidationError[] = [];
- get innerErrors() {
- return this._innerErrors;
- }
-
- constructor(message: string, innerErrors?: DocumentValidationError[]) {
- super(message);
- this._innerErrors = innerErrors;
- }
+/** Represents an error when validating a document against a printer's capabilities. */
+export class TranspileDocumentError extends WebZlpError {
+ private _innerErrors: TranspileDocumentError[] = [];
+ get innerErrors() {
+ return this._innerErrors;
+ }
+
+ constructor(message: string, innerErrors?: TranspileDocumentError[]) {
+ super(message);
+ this._innerErrors = innerErrors ?? [];
+ }
}
diff --git a/src/Printers/Languages/ZplPrinterCommandSet.ts b/src/Printers/Languages/ZplPrinterCommandSet.ts
index 87ff44e..40e7278 100644
--- a/src/Printers/Languages/ZplPrinterCommandSet.ts
+++ b/src/Printers/Languages/ZplPrinterCommandSet.ts
@@ -1,554 +1,540 @@
import * as Options from '../Configuration/PrinterOptions.js';
import {
- PrinterCommandSet,
- TranspilationFormMetadata,
- TranspileCommandDelegate,
- CommandFormInclusionMode
+ PrinterCommandSet,
+ TranspiledDocumentState,
+ type IPrinterExtendedCommandMapping,
+ exhaustiveMatchGuard
} from './PrinterCommandSet.js';
import * as Commands from '../../Documents/Commands.js';
import { AutodetectedPrinter, PrinterModel } from '../Models/PrinterModel.js';
import { PrinterModelDb } from '../Models/PrinterModelDb.js';
-import { PrinterCommunicationOptions } from '../PrinterCommunicationOptions.js';
+import { clampToRange } from '../../NumericRange.js';
export class ZplPrinterCommandSet extends PrinterCommandSet {
- private encoder = new TextEncoder();
-
- get commandLanguage(): Options.PrinterCommandLanguage {
- return Options.PrinterCommandLanguage.zpl;
- }
-
- get formStartCommand(): Uint8Array {
- // All ZPL documents start with the start-of-document command.
- return this.encodeCommand('\n^XA\n');
- }
-
- get formEndCommand(): Uint8Array {
- // All ZPL documents end with the end-of-document command.
- return this.encodeCommand('\n^XZ\n');
- }
-
- encodeCommand(str = '', withNewline = true): Uint8Array {
- // TODO: ZPL supports omitting the newline, figure out a clever way to
- // handle situations where newlines are optional to reduce line noise.
- return this.encoder.encode(str + (withNewline ? '\n' : ''));
- }
-
- protected nonFormCommands: (symbol | Commands.CommandType)[] = [
- Commands.CommandType.AutosenseLabelDimensionsCommand,
- Commands.CommandType.PrintConfigurationCommand,
- Commands.CommandType.RawDocumentCommand,
- Commands.CommandType.RebootPrinterCommand,
- Commands.CommandType.SetDarknessCommand
- ];
-
- protected transpileCommandMap = new Map<
- symbol | Commands.CommandType,
- TranspileCommandDelegate
- >([
- /* eslint-disable prettier/prettier */
- // Ghost commands which shouldn't make it this far.
- [Commands.CommandType.NewLabelCommand, this.unprocessedCommand],
- [Commands.CommandType.CommandCustomSpecificCommand, this.unprocessedCommand],
- [Commands.CommandType.CommandLanguageSpecificCommand, this.unprocessedCommand],
- // Actually valid commands to parse
- [Commands.CommandType.OffsetCommand, this.modifyOffset],
+ private encoder = new TextEncoder();
+
+ get commandLanguage(): Options.PrinterCommandLanguage {
+ return Options.PrinterCommandLanguage.zpl;
+ }
+
+ get formStartCommand(): Uint8Array {
+ // All ZPL documents start with the start-of-document command.
+ return this.encodeCommand('\n^XA\n');
+ }
+
+ get formEndCommand(): Uint8Array {
+ // All ZPL documents end with the end-of-document command.
+ return this.encodeCommand('\n^XZ\n');
+ }
+
+ encodeCommand(str = '', withNewline = true): Uint8Array {
+ // TODO: ZPL supports omitting the newline, figure out a clever way to
+ // handle situations where newlines are optional to reduce line noise.
+ return this.encoder.encode(str + (withNewline ? '\n' : ''));
+ }
+
+ protected nonFormCommands: (symbol | Commands.CommandType)[] = [
+ 'AutosenseLabelDimensionsCommand',
+ 'PrintConfigurationCommand',
+ 'RawDocumentCommand',
+ 'RebootPrinterCommand',
+ 'SetDarknessCommand'
+ ];
+
+ constructor(
+ extendedCommands: Array> = []
+ ) {
+ super(Options.PrinterCommandLanguage.zpl, extendedCommands);
+ }
+
+ public transpileCommand(
+ cmd: Commands.IPrinterCommand,
+ docState: TranspiledDocumentState
+ ): Uint8Array {
+ switch (cmd.type) {
+ default:
+ exhaustiveMatchGuard(cmd.type);
+ break;
+ case 'CustomCommand':
+ return this.extendedCommandHandler(cmd, docState);
+ case 'NewLabelCommand':
+ // Should have been compiled out at a higher step.
+ return this.unprocessedCommand(cmd);
+
+ case 'RebootPrinterCommand':
+ return this.encodeCommand('~JR');
+ case 'QueryConfigurationCommand':
+ return this.encodeCommand('^HZA\r\n^HH');
+ case 'PrintConfigurationCommand':
+ return this.encodeCommand('~WC');
+ case 'SaveCurrentConfigurationCommand':
+ return this.encodeCommand('^JUS');
+
+ case 'SetPrintDirectionCommand':
+ return this.setPrintDirectionCommand((cmd as Commands.SetPrintDirectionCommand).upsideDown);
+ case 'SetDarknessCommand':
+ return this.setDarknessCommand((cmd as Commands.SetDarknessCommand).darknessSetting);
+ case 'AutosenseLabelDimensionsCommand':
+ return this.encodeCommand('~JC');
+ case 'SetPrintSpeedCommand':
+ return this.setPrintSpeedCommand(cmd as Commands.SetPrintSpeedCommand);
+ case 'SetLabelDimensionsCommand':
+ return this.setLabelDimensionsCommand(cmd as Commands.SetLabelDimensionsCommand);
+ case 'SetLabelHomeCommand':
+ return this.setLabelHomeCommand(cmd as Commands.SetLabelHomeCommand);
+ case 'SetLabelPrintOriginOffsetCommand':
+ return this.setLabelPrintOriginOffsetCommand(cmd as Commands.SetLabelPrintOriginOffsetCommand);
+ case 'SetLabelToContinuousMediaCommand':
+ return this.setLabelToContinuousMediaCommand(cmd as Commands.SetLabelToContinuousMediaCommand);
+ case 'SetLabelToMarkMediaCommand':
+ return this.setLabelToMarkMediaCommand(cmd as Commands.SetLabelToMarkMediaCommand);
+ case 'SetLabelToWebGapMediaCommand':
+ return this.setLabelToWebGapMediaCommand(cmd as Commands.SetLabelToWebGapMediaCommand);
+
+ case 'ClearImageBufferCommand':
// Clear image buffer isn't a relevant command on ZPL printers.
// Closest equivalent is the ~JP (pause and cancel) or ~JA (cancel all) but both
// affect in-progress printing operations which is unlikely to be desired operation.
// Translate as a no-op.
- [Commands.CommandType.ClearImageBufferCommand, () => this.noop],
- // ZPL doens't have an OOTB cut command except for one printer.
- // Cutter behavior should be managed by the ^MM command instead.
- [Commands.CommandType.CutNowCommand, () => this.noop],
+ return this.noop;
+ case 'SuppressFeedBackupCommand':
// ZPL needs this for every form printed.
- [Commands.CommandType.SuppressFeedBackupCommand, () => this.encodeCommand('^XB')],
+ return this.encodeCommand('^XB');
+ case 'EnableFeedBackupCommand':
// ZPL doesn't have an enable, it just expects XB for every label
// that should not back up.
- [Commands.CommandType.EnableFeedBackupCommand, () => this.noop],
- [Commands.CommandType.RebootPrinterCommand, () => this.encodeCommand('~JR')],
- // HH returns serial as raw text, HZA gets everything but the serial in XML.
- [Commands.CommandType.QueryConfigurationCommand, () => this.encodeCommand('^HZA\r\n^HH')],
- [Commands.CommandType.PrintConfigurationCommand, () => this.encodeCommand('~WC')],
- [Commands.CommandType.SaveCurrentConfigurationCommand, () => this.encodeCommand('^JUS')],
- [Commands.CommandType.SetPrintDirectionCommand, this.setPrintDirectionCommand],
- [Commands.CommandType.SetDarknessCommand, this.setDarknessCommand],
- [Commands.CommandType.SetPrintSpeedCommand, this.setPrintSpeedCommand],
- [Commands.CommandType.AutosenseLabelDimensionsCommand, () => this.encodeCommand('~JC')],
- [Commands.CommandType.SetLabelDimensionsCommand, this.setLabelDimensionsCommand],
- [Commands.CommandType.SetLabelHomeCommand, this.setLabelHomeCommand],
- [Commands.CommandType.SetLabelPrintOriginOffsetCommand, this.setLabelPrintOriginOffsetCommand],
- [Commands.CommandType.SetLabelToContinuousMediaCommand, this.setLabelToContinuousMediaCommand],
- [Commands.CommandType.SetLabelToWebGapMediaCommand, this.setLabelToWebGapMediaCommand],
- [Commands.CommandType.SetLabelToMarkMediaCommand, this.setLabelToMarkMediaCommand],
- [Commands.CommandType.AddImageCommand, this.addImageCommand],
- [Commands.CommandType.AddLineCommand, this.addLineCommand],
- [Commands.CommandType.AddBoxCommand, this.addBoxCommand],
- [Commands.CommandType.PrintCommand, this.printCommand]
- /* eslint-enable prettier/prettier */
- ]);
-
- constructor(
- customCommands: Array<{
- commandType: symbol;
- applicableLanguages: Options.PrinterCommandLanguage;
- transpileDelegate: TranspileCommandDelegate;
- commandInclusionMode: CommandFormInclusionMode;
- }> = []
- ) {
- super();
-
- for (const newCmd of customCommands) {
- if ((newCmd.applicableLanguages & this.commandLanguage) !== this.commandLanguage) {
- // Command declared to not be applicable to this command set, skip it.
- continue;
- }
-
- this.transpileCommandMap.set(newCmd.commandType, newCmd.transpileDelegate);
- if (newCmd.commandInclusionMode !== CommandFormInclusionMode.sharedForm) {
- this.nonFormCommands.push(newCmd.commandType);
- }
- }
- }
-
- parseConfigurationResponse(
- rawText: string,
- commOpts: PrinterCommunicationOptions
- ): Options.PrinterOptions {
- if (rawText.length <= 0) {
- return Options.PrinterOptions.invalid();
- }
-
- // The two commands run were ^HH to get the raw two-column config label, and ^HZA to get the
- // full XML configuration block. Unfortunately ZPL doesn't seem to put the serial number in
- // the XML so we must pull it from the first line of the raw two-column config.
-
- // Fascinatingly, it doesn't matter what order the two commands appear in. The XML will be
- // presented first and the raw label afterwards.
- const pivotText = '\r\n';
- const pivot = rawText.lastIndexOf(pivotText) + pivotText.length;
- if (pivot == pivotText.length - 1) {
- return Options.PrinterOptions.invalid();
- }
-
- const rawConfig = rawText.substring(pivot);
- // First line of the raw config should be the serial, which should be alphanumeric.
- const serial = rawConfig.match(/[A-Z0-9]+/i)[0];
-
- // ZPL configuration is just XML, parse it into an object and then into config.
-
- // For reasons I do not understand printers will tend to send _one_ invalid XML line
- // and it looks like
- // ` ENUM='NONE, AUTO DETECT, TAG-IT, ICODE, PICO, ISO15693, EPC, UID'>`
- // This is supposed to look like
- // ``
- // I don't have the appropriate equipment to determine where the XML tag prefix is being
- // lost. Do a basic find + replace to replace an instance of the exact text with a fixed
- // version instead.
- // TODO: Deeper investigation with more printers?
- const xmlStart = rawText.indexOf("");
- const rawXml = rawText
- .substring(xmlStart, pivot)
- .replace(
- "\n ENUM='NONE, AUTO DETECT, TAG-IT, ICODE, PICO, ISO15693, EPC, UID'>",
- ""
- );
-
- // The rest is straightforward: parse it as an XML document and pull out
- // the data. The format is standardized and semi-self-documenting.
- const parser = new DOMParser();
- const xmldoc = parser.parseFromString(rawXml, 'application/xml');
- const errorNode = xmldoc.querySelector('parsererror');
- if (errorNode) {
- // TODO: Log? Throw?
- return Options.PrinterOptions.invalid();
- }
-
- return this.docToOptions(xmldoc, serial, commOpts);
- }
-
- private docToOptions(
- doc: Document,
- serial: string,
- commOpts: PrinterCommunicationOptions
- ): Options.PrinterOptions {
- // ZPL includes enough information in the document to autodetect the printer's capabilities.
- const rawModel = this.getXmlText(doc, 'MODEL');
- let model: PrinterModel | string = PrinterModelDb.getModel(rawModel);
- if (model == PrinterModel.unknown) {
- // If the database doesn't have this one listed just use the raw name.
- model = rawModel;
- }
- // ZPL rounds, multiplying by 25 gets us to 'inches' in their book.
- // 8 DPM == 200 DPI, for example.
- const dpi = parseInt(this.getXmlText(doc, 'DOTS-PER-MM')) * 25;
- // Max darkness is an attribute on the element
- const maxDarkness = parseInt(
- doc.getElementsByTagName('MEDIA-DARKNESS')[0].getAttribute('MAX').valueOf()
- );
-
- // Speed table is specially constructed with a few rules.
- // Each table should have at least an auto, min, and max value. We assume we can use the whole
- // number speeds between the min and max values. If the min and max values are the same though
- // that indicates a mobile printer.
- const printSpeedElement = doc.getElementsByTagName('PRINT-RATE')[0];
- const slewSpeedElement = doc.getElementsByTagName('SLEW-RATE')[0];
- // Highest minimum wins
- const printMin = parseInt(printSpeedElement.getAttribute('MIN').valueOf());
- const slewMin = parseInt(slewSpeedElement.getAttribute('MIN').valueOf());
- const speedMin = printMin >= slewMin ? printMin : slewMin;
- const printMax = parseInt(printSpeedElement.getAttribute('MAX').valueOf());
- const slewMax = parseInt(slewSpeedElement.getAttribute('MAX').valueOf());
- const speedMax = printMax <= slewMax ? printMax : slewMax;
-
- const modelInfo = new AutodetectedPrinter(
- Options.PrinterCommandLanguage.zpl,
- dpi,
- model,
- this.getSpeedTable(speedMin, speedMax),
- maxDarkness
- );
-
- const options = new Options.PrinterOptions(
- serial,
- modelInfo,
- this.getXmlText(doc, 'FIRMWARE-VERSION')
- );
-
- const currentDarkness = parseInt(this.getXmlCurrent(doc, 'MEDIA-DARKNESS'));
- const rawDarkness = Math.ceil(currentDarkness * (100 / maxDarkness));
- options.darknessPercent = Math.max(0, Math.min(rawDarkness, 99)) as Options.DarknessPercent;
-
- const printRate = parseInt(this.getXmlText(doc, 'PRINT-RATE'));
- const slewRate = parseInt(this.getXmlText(doc, 'SLEW-RATE'));
- options.speed = new Options.PrintSpeedSettings(
- Options.PrintSpeedSettings.getSpeedFromWholeNumber(printRate),
- Options.PrintSpeedSettings.getSpeedFromWholeNumber(slewRate)
- );
-
- // Always in dots
- const labelWidth = parseInt(this.getXmlCurrent(doc, 'PRINT-WIDTH'));
- const labelLength = parseInt(this.getXmlText(doc, 'LABEL-LENGTH'));
- const labelRoundingStep = commOpts.labelDimensionRoundingStep ?? 0;
- if (labelRoundingStep > 0) {
- // Label size should be rounded to the step value by round-tripping the value to an inch
- // then rounding, then back to dots.
- const roundedWidth = this.roundToNearestStep(
- labelWidth / options.model.dpi,
- labelRoundingStep
- );
- options.labelWidthDots = roundedWidth * options.model.dpi;
- const roundedHeight = this.roundToNearestStep(
- labelLength / options.model.dpi,
- labelRoundingStep
- );
- options.labelHeightDots = roundedHeight * options.model.dpi;
- } else {
- // No rounding
- options.labelWidthDots = labelWidth;
- options.labelHeightDots = labelLength;
- }
-
- // Some firmware versions let you store this, some only retain while power is on.
- const labelHorizontalOffset = parseInt(this.getXmlText(doc, 'LABEL-SHIFT')) || 0;
- const labelHeightOffset = parseInt(this.getXmlCurrent(doc, 'LABEL-TOP')) || 0;
- options.labelPrintOriginOffsetDots = {
- left: labelHorizontalOffset,
- top: labelHeightOffset
- };
-
- options.printOrientation =
- this.getXmlText(doc, 'LABEL-REVERSE') === 'Y'
- ? Options.PrintOrientation.inverted
- : Options.PrintOrientation.normal;
-
- options.thermalPrintMode =
- this.getXmlCurrent(doc, 'MEDIA-TYPE') === 'DIRECT-THERMAL'
- ? Options.ThermalPrintMode.direct
- : Options.ThermalPrintMode.transfer;
-
- options.mediaPrintMode = this.parsePrintMode(this.getXmlCurrent(doc, 'PRINT-MODE'));
-
- options.labelGapDetectMode = this.parseMediaType(this.getXmlCurrent(doc, 'MEDIA-TRACKING'));
-
- options.mediaPrintMode =
- this.getXmlCurrent(doc, 'PRE-PEEL') === 'Y'
- ? Options.MediaPrintMode.peelWithPrepeel
- : options.mediaPrintMode;
-
- // TODO: more hardware options:
- // - Figure out how to encode C{num} for cut-after-label-count
-
- // TODO other options:
- // Autosense settings?
- // Character set?
- // Error handling?
- // Continuous media?
- // Black mark printing?
- // Media feed on powerup settings?
- // Prepeel rewind?
-
- return options;
- }
-
- private range(start: number, stop: number, step = 1) {
- return Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
- }
-
- private getXmlText(doc: Document, tag: string) {
- return doc.getElementsByTagName(tag)[0].textContent;
- }
-
- private getXmlCurrent(doc: Document, tag: string) {
- return doc.getElementsByTagName(tag)[0].getElementsByTagName('CURRENT')[0].textContent;
- }
-
- private parsePrintMode(str: string) {
- switch (str) {
- case 'REWIND':
- return Options.MediaPrintMode.rewind;
- case 'PEEL OFF':
- return Options.MediaPrintMode.peel;
- case 'CUTTER':
- return Options.MediaPrintMode.cutter;
- default:
- case 'TEAR OFF':
- return Options.MediaPrintMode.tearoff;
- }
- }
-
- private parseMediaType(str: string) {
- switch (str) {
- case 'CONTINUOUS':
- return Options.LabelMediaGapDetectionMode.continuous;
- case 'NONCONT-MARK':
- return Options.LabelMediaGapDetectionMode.markSensing;
- default:
- case 'NONCONT-WEB':
- return Options.LabelMediaGapDetectionMode.webSensing;
- }
- }
-
- private getSpeedTable(min: number, max: number) {
- const table = new Map([
- [Options.PrintSpeed.ipsAuto, 0],
- [Options.PrintSpeed.ipsPrinterMin, min],
- [Options.PrintSpeed.ipsPrinterMax, max]
- ]);
- this.range(min, max).forEach((s) =>
- table.set(Options.PrintSpeedSettings.getSpeedFromWholeNumber(s), s)
- );
- return table;
- }
-
- private getFieldOffsetCommand(
- formMetadata: TranspilationFormMetadata,
- additionalHorizontal = 0,
- additionalVertical = 0
- ) {
- const xOffset = Math.trunc(formMetadata.horizontalOffset + additionalHorizontal);
- const yOffset = Math.trunc(formMetadata.verticalOffset + additionalVertical);
- return `^FO${xOffset},${yOffset}`;
- }
-
- private addImageCommand(
- cmd: Commands.AddImageCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- if (cmd?.bitmap == null) {
- return cmdSet.noop;
- }
-
- // ZPL inverts colors. 1 means black, 0 means white. I don't know why.
- const bitmap = cmd.bitmap.toInvertedGRF();
-
- // ZPL supports compressed binary on pretty much all firmwares, default to that.
- // TODO: ASCII-compressed formats are only supported on newer firmwares.
- // Implement feature detection into the transpiler operation to choose the most
- // appropriate compression format such as LZ77/DEFLATE compression for Z64.
- const buffer = bitmap.toZebraCompressedGRF();
-
- // Because the image may be trimmed add an offset command to position to the image data.
- const fieldStart = cmdSet.getFieldOffsetCommand(
- outDoc,
- bitmap.boundingBox.paddingLeft,
- bitmap.boundingBox.paddingTop
- );
-
- const byteLen = bitmap.bytesUncompressed;
- const graphicCmd = `^GFA,${byteLen},${byteLen},${bitmap.bytesPerRow},${buffer}`;
-
- const fieldEnd = '^FS';
-
- // Finally, bump the document offset according to the image height.
- outDoc.verticalOffset += bitmap.boundingBox.height;
-
- return cmdSet.encodeCommand(fieldStart + graphicCmd + fieldEnd);
- }
+ return this.noop;
+
+ case 'OffsetCommand':
+ return this.modifyOffset(cmd as Commands.OffsetCommand, docState, this);
+ case 'RawDocumentCommand':
+ return this.encodeCommand((cmd as Commands.RawDocumentCommand).rawDocument, false);
+ case 'AddBoxCommand':
+ return this.addBoxCommand(cmd as Commands.AddBoxCommand, docState);
+ case 'AddImageCommand':
+ return this.addImageCommand(cmd as Commands.AddImageCommand, docState);
+ case 'AddLineCommand':
+ return this.addLineCommand(cmd as Commands.AddLineCommand, docState);
+ case 'CutNowCommand':
+ // ZPL doesn't have an OOTB cut command except for one printer.
+ // Cutter behavior should be managed by the ^MM command instead.
+ return this.noop;
- private setPrintDirectionCommand(
- cmd: Commands.SetPrintDirectionCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- const dir = cmd.upsideDown ? 'I' : 'N';
- return cmdSet.encodeCommand(`^PO${dir}`);
+ case 'PrintCommand':
+ return this.printCommand(cmd as Commands.PrintCommand);
}
-
- private setDarknessCommand(
- cmd: Commands.SetDarknessCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- const dark = Math.trunc(cmd.darknessSetting);
- return cmdSet.encodeCommand(`~SD${dark}`);
+ }
+
+ parseConfigurationResponse(
+ rawText: string,
+ mediaOptions: Options.IPrinterLabelMediaOptions,
+ ): Options.PrinterOptions {
+ if (rawText.length <= 0) {
+ return Options.PrinterOptions.invalid;
}
- private setPrintSpeedCommand(
- cmd: Commands.SetPrintSpeedCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- // ZPL uses separate print, slew, and backfeed speeds. Re-use print for backfeed.
- return cmdSet.encodeCommand(`^PR${cmd.speedVal},${cmd.mediaSpeedVal},${cmd.speedVal}`);
- }
+ // The two commands run were ^HH to get the raw two-column config label, and ^HZA to get the
+ // full XML configuration block. Unfortunately ZPL doesn't seem to put the serial number in
+ // the XML so we must pull it from the first line of the raw two-column config.
- private setLabelDimensionsCommand(
- cmd: Commands.SetLabelDimensionsCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- const width = Math.trunc(cmd.widthInDots);
- const widthCmd = cmdSet.encodeCommand(`^PW${width}`);
- if (cmd.setsHeight) {
- const height = Math.trunc(cmd.heightInDots);
- const heightCmd = cmdSet.encodeCommand(`^LL${height},N`);
- return cmdSet.combineCommands(widthCmd, heightCmd);
- }
- return widthCmd;
+ // Fascinatingly, it doesn't matter what order the two commands appear in. The XML will be
+ // presented first and the raw label afterwards.
+ const pivotText = '\r\n';
+ const pivot = rawText.lastIndexOf(pivotText) + pivotText.length;
+ if (pivot == pivotText.length - 1) {
+ return Options.PrinterOptions.invalid;
}
- private setLabelHomeCommand(
- cmd: Commands.SetLabelHomeCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- const xOffset = Math.trunc(cmd.xOffset);
- const yOffset = Math.trunc(cmd.yOffset);
- return cmdSet.encodeCommand(`^LH${xOffset},${yOffset}`);
+ const rawConfig = rawText.substring(pivot);
+ // First line of the raw config should be the serial, which should be alphanumeric.
+ const serial = rawConfig.match(/[A-Z0-9]+/i)?.at(0) ?? 'no_serial_nm';
+
+ // ZPL configuration is just XML, parse it into an object and then into config.
+
+ // For reasons I do not understand printers will tend to send _one_ invalid XML line
+ // and it looks like
+ // ` ENUM='NONE, AUTO DETECT, TAG-IT, ICODE, PICO, ISO15693, EPC, UID'>`
+ // This is supposed to look like
+ // ``
+ // I don't have the appropriate equipment to determine where the XML tag prefix is being
+ // lost. Do a basic find + replace to replace an instance of the exact text with a fixed
+ // version instead.
+ // TODO: Deeper investigation with more printers?
+ const xmlStart = rawText.indexOf("");
+ const rawXml = rawText
+ .substring(xmlStart, pivot)
+ .replace(
+ "\n ENUM='NONE, AUTO DETECT, TAG-IT, ICODE, PICO, ISO15693, EPC, UID'>",
+ ""
+ );
+
+ // The rest is straightforward: parse it as an XML document and pull out
+ // the data. The format is standardized and semi-self-documenting.
+ const parser = new DOMParser();
+ const xmlDoc = parser.parseFromString(rawXml, 'application/xml');
+ const errorNode = xmlDoc.querySelector('parsererror');
+ if (errorNode) {
+ // TODO: Log? Throw?
+ return Options.PrinterOptions.invalid;
}
- private setLabelPrintOriginOffsetCommand(
- cmd: Commands.SetLabelPrintOriginOffsetCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- // This ends up being two commands, one to set the top and one to set the
- // horizontal shift. LS moves the horizontal, LT moves the top. LT is
- // clamped to +/- 120 dots, horizontal is 9999.
- const xOffset = cmdSet.clampToRange(Math.trunc(cmd.xOffset), -9999, 9999);
- const yOffset = cmdSet.clampToRange(Math.trunc(cmd.yOffset), -120, 120);
- return cmdSet.encodeCommand(`^LS${xOffset}^LT${yOffset}`);
+ return this.docToOptions(xmlDoc, serial, mediaOptions);
+ }
+
+ private docToOptions(
+ doc: Document,
+ serial: string,
+ mediaOptions: Options.IPrinterLabelMediaOptions
+ ): Options.PrinterOptions {
+ // ZPL includes enough information in the document to autodetect the printer's capabilities.
+ const rawModel = this.getXmlText(doc, 'MODEL') ?? 'UNKNOWN_ZPL';
+ const model = PrinterModelDb.getModel(rawModel);
+
+ // ZPL rounds, multiplying by 25 gets us to 'inches' in their book.
+ // 8 DPM == 200 DPI, for example.
+ const dpi = parseInt(this.getXmlText(doc, 'DOTS-PER-MM') ?? '8') * 25;
+ // Max darkness is an attribute on the element
+ const maxDarkness = parseInt(
+ doc.getElementsByTagName('MEDIA-DARKNESS').item(0)?.getAttribute('MAX')?.valueOf() ?? '30'
+ );
+
+ // Speed table is specially constructed with a few rules.
+ // Each table should have at least an auto, min, and max value. We assume we can use the whole
+ // number speeds between the min and max values. If the min and max values are the same though
+ // that indicates a mobile printer.
+ const printSpeedElement = doc.getElementsByTagName('PRINT-RATE').item(0);
+ const slewSpeedElement = doc.getElementsByTagName('SLEW-RATE').item(0);
+ const speedDefault = '0';
+ // Highest minimum wins
+ const printMin = parseInt(printSpeedElement?.getAttribute('MIN')?.valueOf() ?? speedDefault);
+ const slewMin = parseInt(slewSpeedElement?.getAttribute('MIN')?.valueOf() ?? speedDefault);
+ const speedMin = printMin >= slewMin ? printMin : slewMin;
+ // Lowest max wins
+ const printMax = parseInt(printSpeedElement?.getAttribute('MAX')?.valueOf() ?? speedDefault);
+ const slewMax = parseInt(slewSpeedElement?.getAttribute('MAX')?.valueOf() ?? speedDefault);
+ const speedMax = printMax <= slewMax ? printMax : slewMax;
+
+ const modelInfo = new AutodetectedPrinter(
+ Options.PrinterCommandLanguage.zpl,
+ dpi,
+ model === PrinterModel.unknown ? rawModel : model,
+ this.getSpeedTable(speedMin, speedMax),
+ maxDarkness
+ );
+
+ const options = new Options.PrinterOptions(
+ serial,
+ modelInfo,
+ this.getXmlText(doc, 'FIRMWARE-VERSION') ?? ''
+ );
+
+ const currentDarkness = parseInt(this.getXmlCurrent(doc, 'MEDIA-DARKNESS') ?? '15');
+ const rawDarkness = Math.ceil(currentDarkness * (100 / maxDarkness));
+ options.darknessPercent = Math.max(0, Math.min(rawDarkness, 99)) as Options.DarknessPercent;
+
+ const printRate = parseInt(this.getXmlText(doc, 'PRINT-RATE') ?? '1');
+ const slewRate = parseInt(this.getXmlText(doc, 'SLEW-RATE') ?? '1');
+ options.speed = new Options.PrintSpeedSettings(
+ Options.PrintSpeedSettings.getSpeedFromWholeNumber(printRate),
+ Options.PrintSpeedSettings.getSpeedFromWholeNumber(slewRate)
+ );
+
+ // Always in dots
+ const labelWidth = parseInt(this.getXmlCurrent(doc, 'PRINT-WIDTH') ?? '200');
+ const labelLength = parseInt(this.getXmlText(doc, 'LABEL-LENGTH') ?? '200');
+ const labelRoundingStep = mediaOptions.labelDimensionRoundingStep ?? 0;
+ if (labelRoundingStep > 0) {
+ // Label size should be rounded to the step value by round-tripping the value to an inch
+ // then rounding, then back to dots.
+ const roundedWidth = this.roundToNearestStep(
+ labelWidth / options.model.dpi,
+ labelRoundingStep
+ );
+ options.labelWidthDots = roundedWidth * options.model.dpi;
+ const roundedHeight = this.roundToNearestStep(
+ labelLength / options.model.dpi,
+ labelRoundingStep
+ );
+ options.labelHeightDots = roundedHeight * options.model.dpi;
+ } else {
+ // No rounding
+ options.labelWidthDots = labelWidth;
+ options.labelHeightDots = labelLength;
}
- private setLabelToContinuousMediaCommand(
- cmd: Commands.SetLabelToContinuousMediaCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- const length = Math.trunc(cmd.labelLengthInDots);
- const gap = Math.trunc(cmd.labelGapInDots);
- return cmdSet.encodeCommand(`^MNN^LL${length + gap}`);
+ // Some firmware versions let you store this, some only retain while power is on.
+ const labelHorizontalOffset = parseInt(this.getXmlText(doc, 'LABEL-SHIFT') ?? '0') || 0;
+ const labelHeightOffset = parseInt(this.getXmlCurrent(doc, 'LABEL-TOP') ?? '0') || 0;
+ options.labelPrintOriginOffsetDots = {
+ left: labelHorizontalOffset,
+ top: labelHeightOffset
+ };
+
+ options.printOrientation =
+ this.getXmlText(doc, 'LABEL-REVERSE') === 'Y'
+ ? Options.PrintOrientation.inverted
+ : Options.PrintOrientation.normal;
+
+ options.thermalPrintMode =
+ this.getXmlCurrent(doc, 'MEDIA-TYPE') === 'DIRECT-THERMAL'
+ ? Options.ThermalPrintMode.direct
+ : Options.ThermalPrintMode.transfer;
+
+ options.mediaPrintMode = this.parsePrintMode(this.getXmlCurrent(doc, 'PRINT-MODE') ?? '');
+
+ options.labelGapDetectMode = this.parseMediaType(this.getXmlCurrent(doc, 'MEDIA-TRACKING') ?? '');
+
+ options.mediaPrintMode =
+ this.getXmlCurrent(doc, 'PRE-PEEL') === 'Y'
+ ? Options.MediaPrintMode.peelWithPrePeel
+ : options.mediaPrintMode;
+
+ // TODO: more hardware options:
+ // - Figure out how to encode C{num} for cut-after-label-count
+
+ // TODO other options:
+ // Autosense settings?
+ // Character set?
+ // Error handling?
+ // Continuous media?
+ // Black mark printing?
+ // Media feed on power up settings?
+ // Pre-peel rewind?
+
+ return options;
+ }
+
+ private range(start: number, stop: number, step = 1) {
+ return Array.from({ length: (stop - start) / step + 1 }, (_, i) => start + i * step);
+ }
+
+ private getXmlText(doc: Document, tag: string) {
+ return doc.getElementsByTagName(tag).item(0)?.textContent ?? undefined;
+ }
+
+ private getXmlCurrent(doc: Document, tag: string) {
+ return doc.getElementsByTagName(tag).item(0)
+ ?.getElementsByTagName('CURRENT').item(0)
+ ?.textContent ?? undefined;
+ }
+
+ private parsePrintMode(str: string) {
+ switch (str) {
+ case 'REWIND':
+ return Options.MediaPrintMode.rewind;
+ case 'PEEL OFF':
+ return Options.MediaPrintMode.peel;
+ case 'CUTTER':
+ return Options.MediaPrintMode.cutter;
+ default:
+ case 'TEAR OFF':
+ return Options.MediaPrintMode.tearOff;
}
-
- private setLabelToWebGapMediaCommand(
- cmd: Commands.SetLabelToWebGapMediaCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- const length = Math.trunc(cmd.labelLengthInDots);
- return cmdSet.encodeCommand(`^MNY^LL${length},Y`);
- }
-
- private setLabelToMarkMediaCommand(
- cmd: Commands.SetLabelToMarkMediaCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- const length = Math.trunc(cmd.labelLengthInDots);
- const lineOffset = Math.trunc(cmd.blackLineOffset);
- return cmdSet.encodeCommand(`^MNM,${length}^LL${lineOffset}`);
+ }
+
+ private parseMediaType(str: string) {
+ switch (str) {
+ case 'CONTINUOUS':
+ return Options.LabelMediaGapDetectionMode.continuous;
+ case 'NONCONT-MARK':
+ return Options.LabelMediaGapDetectionMode.markSensing;
+ default:
+ case 'NONCONT-WEB':
+ return Options.LabelMediaGapDetectionMode.webSensing;
}
-
- private printCommand(
- cmd: Commands.PrintCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- // TODO: Make sure this actually works this way..
- // According to the docs the first parameter is "total" labels,
- // while the third is duplicates.
- const total = Math.trunc(cmd.count * (cmd.additionalDuplicateOfEach + 1));
- const dup = Math.trunc(cmd.additionalDuplicateOfEach);
- return cmdSet.encodeCommand(`^PQ${total},0,${dup},N`);
- }
-
- private addLineCommand(
- cmd: Commands.AddLineCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- return cmdSet.lineOrBoxToCmd(
- cmdSet,
- outDoc,
- cmd.heightInDots,
- cmd.lengthInDots,
- cmd.color,
- // A line is just a box filled in!
- Math.min(cmd.heightInDots, cmd.lengthInDots)
- );
+ }
+
+ private getSpeedTable(min: number, max: number) {
+ return new Map([
+ [Options.PrintSpeed.ipsAuto, 0],
+ [Options.PrintSpeed.ipsPrinterMin, min],
+ [Options.PrintSpeed.ipsPrinterMax, max],
+ ...this.range(min, max).map(s =>
+ [Options.PrintSpeedSettings.getSpeedFromWholeNumber(s), s] as [Options.PrintSpeed, number])
+ ]);
+ }
+
+ private getFieldOffsetCommand(
+ formMetadata: TranspiledDocumentState,
+ additionalHorizontal = 0,
+ additionalVertical = 0
+ ) {
+ const xOffset = Math.trunc(formMetadata.horizontalOffset + additionalHorizontal);
+ const yOffset = Math.trunc(formMetadata.verticalOffset + additionalVertical);
+ return `^FO${xOffset},${yOffset}`;
+ }
+
+ private addImageCommand(
+ cmd: Commands.AddImageCommand,
+ outDoc: TranspiledDocumentState
+ ): Uint8Array {
+ // ZPL treats colors as print element enable. 1 means black, 0 means white.
+ const bitmap = cmd.bitmap;
+ // TODO: support image conversion options.
+ //const imageOptions = cmd.imageConversionOptions;
+
+ // ZPL supports compressed binary on pretty much all firmware, default to that.
+ // TODO: ASCII-compressed formats are only supported on newer firmware.
+ // Implement feature detection into the transpiler operation to choose the most
+ // appropriate compression format such as LZ77/DEFLATE compression for Z64.
+ const buffer = bitmap.toZebraCompressedGRF();
+
+ // Because the image may be trimmed add an offset command to position to the image data.
+ const fieldStart = this.getFieldOffsetCommand(
+ outDoc,
+ bitmap.boundingBox.paddingLeft,
+ bitmap.boundingBox.paddingTop
+ );
+
+ const byteLen = bitmap.bytesUncompressed;
+ const graphicCmd = `^GFA,${byteLen},${byteLen},${bitmap.bytesPerRow},${buffer}`;
+
+ const fieldEnd = '^FS';
+
+ // Finally, bump the document offset according to the image height.
+ outDoc.verticalOffset += bitmap.boundingBox.height;
+
+ return this.encodeCommand(fieldStart + graphicCmd + fieldEnd);
+ }
+
+ private setPrintDirectionCommand(
+ upsideDown: boolean
+ ): Uint8Array {
+ const dir = upsideDown ? 'I' : 'N';
+ return this.encodeCommand(`^PO${dir}`);
+ }
+
+ private setDarknessCommand(
+ darkness: number
+ ): Uint8Array {
+ const dark = Math.trunc(darkness);
+ return this.encodeCommand(`~SD${dark}`);
+ }
+
+ private setPrintSpeedCommand(
+ cmd: Commands.SetPrintSpeedCommand,
+ ): Uint8Array {
+ // ZPL uses separate print, slew, and backfeed speeds. Re-use print for backfeed.
+ return this.encodeCommand(`^PR${cmd.speedVal},${cmd.mediaSpeedVal},${cmd.speedVal}`);
+ }
+
+ private setLabelDimensionsCommand(
+ cmd: Commands.SetLabelDimensionsCommand
+ ): Uint8Array {
+ const width = Math.trunc(cmd.widthInDots);
+ const widthCmd = this.encodeCommand(`^PW${width}`);
+ if (cmd.setsHeight && cmd.heightInDots !== undefined && cmd.gapLengthInDots !== undefined) {
+ const height = Math.trunc(cmd.heightInDots);
+ const heightCmd = this.encodeCommand(`^LL${height},N`);
+ return this.combineCommands(widthCmd, heightCmd);
}
-
- private addBoxCommand(
- cmd: Commands.AddBoxCommand,
- outDoc: TranspilationFormMetadata,
- cmdSet: ZplPrinterCommandSet
- ): Uint8Array {
- return cmdSet.lineOrBoxToCmd(
- cmdSet,
- outDoc,
- cmd.heightInDots,
- cmd.lengthInDots,
- Commands.DrawColor.black,
- cmd.thickness
- );
+ return widthCmd;
+ }
+
+ private setLabelHomeCommand(
+ cmd: Commands.SetLabelHomeCommand
+ ): Uint8Array {
+ const xOffset = Math.trunc(cmd.xOffset);
+ const yOffset = Math.trunc(cmd.yOffset);
+ return this.encodeCommand(`^LH${xOffset},${yOffset}`);
+ }
+
+ private setLabelPrintOriginOffsetCommand(
+ cmd: Commands.SetLabelPrintOriginOffsetCommand
+ ): Uint8Array {
+ // This ends up being two commands, one to set the top and one to set the
+ // horizontal shift. LS moves the horizontal, LT moves the top. LT is
+ // clamped to +/- 120 dots, horizontal is 9999.
+ const xOffset = clampToRange(Math.trunc(cmd.xOffset), -9999, 9999);
+ const yOffset = clampToRange(Math.trunc(cmd.yOffset), -120, 120);
+ return this.encodeCommand(`^LS${xOffset}^LT${yOffset}`);
+ }
+
+ private setLabelToContinuousMediaCommand(
+ cmd: Commands.SetLabelToContinuousMediaCommand
+ ): Uint8Array {
+ const length = Math.trunc(cmd.labelLengthInDots);
+ const gap = Math.trunc(cmd.labelGapInDots);
+ return this.encodeCommand(`^MNN^LL${length + gap}`);
+ }
+
+ private setLabelToWebGapMediaCommand(
+ cmd: Commands.SetLabelToWebGapMediaCommand
+ ): Uint8Array {
+ const length = Math.trunc(cmd.labelLengthInDots);
+ return this.encodeCommand(`^MNY^LL${length},Y`);
+ }
+
+ private setLabelToMarkMediaCommand(
+ cmd: Commands.SetLabelToMarkMediaCommand
+ ): Uint8Array {
+ const length = Math.trunc(cmd.labelLengthInDots);
+ const lineOffset = Math.trunc(cmd.blackLineOffset);
+ return this.encodeCommand(`^MNM,${length}^LL${lineOffset}`);
+ }
+
+ private printCommand(
+ cmd: Commands.PrintCommand
+ ): Uint8Array {
+ // TODO: Make sure this actually works this way..
+ // According to the docs the first parameter is "total" labels,
+ // while the third is duplicates.
+ const total = Math.trunc(cmd.count * (cmd.additionalDuplicateOfEach + 1));
+ const dup = Math.trunc(cmd.additionalDuplicateOfEach);
+ return this.encodeCommand(`^PQ${total},0,${dup},N`);
+ }
+
+ private addLineCommand(
+ cmd: Commands.AddLineCommand,
+ outDoc: TranspiledDocumentState
+ ): Uint8Array {
+ return this.lineOrBoxToCmd(
+ outDoc,
+ cmd.heightInDots,
+ cmd.lengthInDots,
+ cmd.color,
+ // A line is just a box filled in!
+ Math.min(cmd.heightInDots, cmd.lengthInDots)
+ );
+ }
+
+ private addBoxCommand(
+ cmd: Commands.AddBoxCommand,
+ outDoc: TranspiledDocumentState,
+ ): Uint8Array {
+ return this.lineOrBoxToCmd(
+ outDoc,
+ cmd.heightInDots,
+ cmd.lengthInDots,
+ Commands.DrawColor.black,
+ cmd.thickness
+ );
+ }
+
+ private lineOrBoxToCmd(
+ outDoc: TranspiledDocumentState,
+ height: number,
+ length: number,
+ color: Commands.DrawColor,
+ thickness?: number
+ ): Uint8Array {
+ height = Math.trunc(height) || 0;
+ thickness = Math.trunc(thickness ?? 1) || 1;
+ length = Math.trunc(length) || 0;
+ let drawMode: string;
+ switch (color) {
+ case Commands.DrawColor.black:
+ drawMode = 'B';
+ break;
+ case Commands.DrawColor.white:
+ drawMode = 'W';
+ break;
}
+ const fieldStart = this.getFieldOffsetCommand(outDoc);
- private lineOrBoxToCmd(
- cmdSet: ZplPrinterCommandSet,
- outDoc: TranspilationFormMetadata,
- height: number,
- length: number,
- color: Commands.DrawColor,
- thickness?: number
- ): Uint8Array {
- height = Math.trunc(height) || 0;
- thickness = Math.trunc(thickness) || 1;
- length = Math.trunc(length) || 0;
- let drawMode: string;
- switch (color) {
- case Commands.DrawColor.black:
- drawMode = 'B';
- break;
- case Commands.DrawColor.white:
- drawMode = 'W';
- break;
- }
- const fieldStart = cmdSet.getFieldOffsetCommand(outDoc);
-
- // TODO: Support rounding?
- return cmdSet.encodeCommand(
- [fieldStart, `^GB${length}`, height, thickness, drawMode, '^FS'].join(',')
- );
- }
+ // TODO: Support rounding?
+ return this.encodeCommand(
+ [fieldStart, `^GB${length}`, height, thickness, drawMode, '^FS'].join(',')
+ );
+ }
}
diff --git a/src/Printers/Models/EplPrinterModels.ts b/src/Printers/Models/EplPrinterModels.ts
index 674026e..6eff466 100644
--- a/src/Printers/Models/EplPrinterModels.ts
+++ b/src/Printers/Models/EplPrinterModels.ts
@@ -3,57 +3,57 @@ import { PrinterCommandLanguage, PrintSpeed } from '../Configuration/PrinterOpti
/** EPL printers have a lot in common. */
export abstract class EplPrinter extends BasePrinterInfo {
- get commandLanguage(): PrinterCommandLanguage {
- return PrinterCommandLanguage.epl;
- }
+ get commandLanguage(): PrinterCommandLanguage {
+ return PrinterCommandLanguage.epl;
+ }
- get maxDarkness(): number {
- return 15; // EPL max darkness
- }
+ get maxDarkness(): number {
+ return 15; // EPL max darkness
+ }
}
/** 28XX model printers are mostly the same */
export abstract class LP28XX extends EplPrinter {
- get dpi(): number {
- return 203;
- }
-
- abstract get model(): PrinterModel;
-
- private _speedTable = new Map([
- [PrintSpeed.ipsAuto, 3],
- [PrintSpeed.ipsPrinterMax, 4],
- [PrintSpeed.ipsPrinterMin, 1],
- [PrintSpeed.ips1_5, 1],
- [PrintSpeed.ips2, 2],
- [PrintSpeed.ips2_5, 3],
- [PrintSpeed.ips3_5, 4]
- ]);
- get speedTable(): ReadonlyMap {
- return this._speedTable;
- }
+ get dpi(): number {
+ return 203;
+ }
+
+ abstract get model(): PrinterModel;
+
+ private _speedTable = new Map([
+ [PrintSpeed.ipsAuto, 3],
+ [PrintSpeed.ipsPrinterMax, 4],
+ [PrintSpeed.ipsPrinterMin, 1],
+ [PrintSpeed.ips1_5, 1],
+ [PrintSpeed.ips2, 2],
+ [PrintSpeed.ips2_5, 3],
+ [PrintSpeed.ips3_5, 4]
+ ]);
+ get speedTable(): ReadonlyMap {
+ return this._speedTable;
+ }
}
export class LP2844 extends LP28XX {
- get model() {
- return PrinterModel.lp2824;
- }
+ get model() {
+ return PrinterModel.lp2824;
+ }
}
export class LP2824 extends LP28XX {
- get model() {
- return PrinterModel.lp2824;
- }
+ get model() {
+ return PrinterModel.lp2824;
+ }
}
export class TLP2824 extends LP28XX {
- get model() {
- return PrinterModel.tlp2824;
- }
+ get model() {
+ return PrinterModel.tlp2824;
+ }
}
export class TLP2844 extends LP28XX {
- get model() {
- return PrinterModel.tlp2844;
- }
+ get model() {
+ return PrinterModel.tlp2844;
+ }
}
diff --git a/src/Printers/Models/PrinterModel.ts b/src/Printers/Models/PrinterModel.ts
index 23ad8ea..1080de6 100644
--- a/src/Printers/Models/PrinterModel.ts
+++ b/src/Printers/Models/PrinterModel.ts
@@ -2,153 +2,153 @@ import { WebZlpError } from '../../WebZlpError.js';
import { PrinterCommandLanguage, PrintSpeed } from '../Configuration/PrinterOptions.js';
export enum PrinterModel {
- unknown = 'unknown',
- zplAutodetect = 'ZPL_AUTODETECT',
- lp2824 = 'LP2824',
- lp2824z = 'LP2824Z',
- lp2844 = 'LP2844',
- lp2844ups = 'LP2844UPS',
- lp2844fedex = 'LP2844FEDEX',
- lp2844z = 'LP2844Z',
- tlp2824 = 'TLP2824',
- tlp2824z = 'TLP2824Z',
- tlp2844 = 'TPL2844',
- tlp2844z = 'TPL2844Z'
+ unknown = 'unknown',
+ zplAutodetect = 'ZPL_AUTODETECT',
+ lp2824 = 'LP2824',
+ lp2824z = 'LP2824Z',
+ lp2844 = 'LP2844',
+ lp2844ups = 'LP2844UPS',
+ lp2844fedex = 'LP2844FEDEX',
+ lp2844z = 'LP2844Z',
+ tlp2824 = 'TLP2824',
+ tlp2824z = 'TLP2824Z',
+ tlp2844 = 'TPL2844',
+ tlp2844z = 'TPL2844Z'
}
export interface IPrinterModelInfo {
- /** Gets the command language for this printer. */
- get commandLanguage(): PrinterCommandLanguage;
+ /** Gets the command language for this printer. */
+ get commandLanguage(): PrinterCommandLanguage;
- /** Gets the DPI of this printer. */
- get dpi(): number;
+ /** Gets the DPI of this printer. */
+ get dpi(): number;
- /** Gets the model of this printer. */
- get model(): PrinterModel | string;
+ /** Gets the model of this printer. */
+ get model(): PrinterModel | string;
- /** Gets the map of speeds this printer supports. */
- get speedTable(): ReadonlyMap;
+ /** Gets the map of speeds this printer supports. */
+ get speedTable(): ReadonlyMap;
- /** Gets the max value of the darkness, to map to a percent. */
- get maxDarkness(): number;
+ /** Gets the max value of the darkness, to map to a percent. */
+ get maxDarkness(): number;
- /** Determine if a given speed will work with this model. */
- isSpeedValid(speed: PrintSpeed): boolean;
+ /** Determine if a given speed will work with this model. */
+ isSpeedValid(speed: PrintSpeed): boolean;
- /** Get the raw value this model understands as the speed. */
- getSpeedValue(speed: PrintSpeed): number | undefined;
+ /** Get the raw value this model understands as the speed. */
+ getSpeedValue(speed: PrintSpeed): number;
- /** Get a print speed for this printer for */
- fromRawSpeed(rawSpeed: number): PrintSpeed;
+ /** Get a print speed for this printer. Defaults to minimum. */
+ fromRawSpeed(rawSpeed?: number): PrintSpeed;
}
export abstract class BasePrinterInfo implements IPrinterModelInfo {
- /** Gets the command language for this printer. */
- abstract get commandLanguage(): PrinterCommandLanguage;
- /** Gets the DPI of this printer. */
- abstract get dpi(): number;
- /** Gets the model of this printer. */
- abstract get model(): PrinterModel | string;
-
- // Speed is determined by what the printer supports
- // EPL printers have a table that determines their setting and it needs to be hardcoded.
- // ZPL printers follow this pattern:
- // 1 = 25.4 mm/sec. (1 inch/sec.)
- // A or 2 = 50.8 mm/sec. (2 inches/sec.)
- // A is the default print and backfeed speed
- // B or 3 = 76.2 mm/sec. (3 inches/sec.)
- // C or 4 = 101.6 mm/sec. (4 inches/sec.)
- // 5 = 127 mm/sec. (5 inches/sec.)
- // D or 6 = 152.4 mm/sec. (6 inches/sec.)
- // D is the default media slew speed
- // 7 = 177.8 mm/sec. (7 inches/sec.)
- // E or 8 = 203.2 mm/sec. (8 inches/sec.)
- // 9 = 220.5 mm/sec. (9 inches/sec.)
- // 10 = 245 mm/sec. (10 inches/sec.)
- // 11 = 269.5 mm/sec. (11 inches/sec.)
- // 12 = 304.8 mm/sec. (12 inches/sec.)
- // 13 = 13 in/sec
- // 14 = 14 in/sec
- // This gets encoded into the speed table.
- // Every speed table should also have entries for ipsPrinterMin, ipsPrinterMax, and auto.
- // These should be duplicate entries of real values in the speed table so that
- // we have sane defaults for commands to default to.
- /** Gets the map of speeds this printer supports. */
- abstract get speedTable(): ReadonlyMap;
- /** Gets the max value of the darkness, to map to a percent. */
- abstract get maxDarkness(): number;
-
- /** Determine if a given speed will work with this model. */
- public isSpeedValid(speed: PrintSpeed): boolean {
- return this.speedTable.has(speed);
- }
-
- /** Get the raw value this model understands as the speed. */
- public getSpeedValue(speed: PrintSpeed): number | undefined {
- const val = this.speedTable.get(speed) ?? this.speedTable[PrintSpeed.ipsAuto];
- return val;
- }
-
- /** Get a print speed for this printer for */
- public fromRawSpeed(rawSpeed: number): PrintSpeed {
- for (const [key, val] of this.speedTable) {
- if (
- val === rawSpeed &&
- key != PrintSpeed.ipsAuto &&
- key != PrintSpeed.ipsPrinterMax &&
- key != PrintSpeed.ipsPrinterMin
- ) {
- return key;
- }
- }
- return PrintSpeed.ipsAuto;
+ /** Gets the command language for this printer. */
+ abstract get commandLanguage(): PrinterCommandLanguage;
+ /** Gets the DPI of this printer. */
+ abstract get dpi(): number;
+ /** Gets the model of this printer. */
+ abstract get model(): PrinterModel | string;
+
+ // Speed is determined by what the printer supports
+ // EPL printers have a table that determines their setting and it needs to be hardcoded.
+ // ZPL printers follow this pattern:
+ // 1 = 25.4 mm/sec. (1 inch/sec.)
+ // A or 2 = 50.8 mm/sec. (2 inches/sec.)
+ // A is the default print and backfeed speed
+ // B or 3 = 76.2 mm/sec. (3 inches/sec.)
+ // C or 4 = 101.6 mm/sec. (4 inches/sec.)
+ // 5 = 127 mm/sec. (5 inches/sec.)
+ // D or 6 = 152.4 mm/sec. (6 inches/sec.)
+ // D is the default media slew speed
+ // 7 = 177.8 mm/sec. (7 inches/sec.)
+ // E or 8 = 203.2 mm/sec. (8 inches/sec.)
+ // 9 = 220.5 mm/sec. (9 inches/sec.)
+ // 10 = 245 mm/sec. (10 inches/sec.)
+ // 11 = 269.5 mm/sec. (11 inches/sec.)
+ // 12 = 304.8 mm/sec. (12 inches/sec.)
+ // 13 = 13 in/sec
+ // 14 = 14 in/sec
+ // This gets encoded into the speed table.
+ // Every speed table should also have entries for ipsPrinterMin, ipsPrinterMax, and auto.
+ // These should be duplicate entries of real values in the speed table so that
+ // we have sane defaults for commands to default to.
+ /** Gets the map of speeds this printer supports. */
+ abstract get speedTable(): ReadonlyMap;
+ /** Gets the max value of the darkness, to map to a percent. */
+ abstract get maxDarkness(): number;
+
+ /** Determine if a given speed will work with this model. */
+ public isSpeedValid(speed: PrintSpeed): boolean {
+ return this.speedTable.has(speed);
+ }
+
+ /** Get the raw value this model understands as the speed. */
+ public getSpeedValue(speed: PrintSpeed): number {
+ const val = this.speedTable.get(speed) ?? this.speedTable.get(PrintSpeed.ipsAuto);
+ return val ?? 0;
+ }
+
+ /** Get a print speed for this printer for */
+ public fromRawSpeed(rawSpeed: number): PrintSpeed {
+ for (const [key, val] of this.speedTable) {
+ if (
+ val === rawSpeed &&
+ key != PrintSpeed.ipsAuto &&
+ key != PrintSpeed.ipsPrinterMax &&
+ key != PrintSpeed.ipsPrinterMin
+ ) {
+ return key;
+ }
}
+ return PrintSpeed.ipsAuto;
+ }
}
/** Class representing a printer that could not be identified. */
export class UnknownPrinter extends BasePrinterInfo {
- get commandLanguage(): PrinterCommandLanguage {
- return PrinterCommandLanguage.none;
- }
- get speedTable(): ReadonlyMap {
- throw new WebZlpError('Unknown printer, cannot read metadata.');
- }
- get model(): PrinterModel {
- return PrinterModel.unknown;
- }
- get dpi(): number {
- throw new WebZlpError('Unknown printer, cannot read metadata.');
- }
- get maxDarkness(): number {
- throw new WebZlpError('Unknown printer, cannot read metadata.');
- }
+ get commandLanguage(): PrinterCommandLanguage {
+ return PrinterCommandLanguage.none;
+ }
+ get speedTable(): ReadonlyMap {
+ throw new WebZlpError('Unknown printer, cannot read metadata.');
+ }
+ get model(): PrinterModel {
+ return PrinterModel.unknown;
+ }
+ get dpi(): number {
+ throw new WebZlpError('Unknown printer, cannot read metadata.');
+ }
+ get maxDarkness(): number {
+ throw new WebZlpError('Unknown printer, cannot read metadata.');
+ }
}
/** A printer model object that was autodetected from the printer itself. */
export class AutodetectedPrinter extends BasePrinterInfo {
- get commandLanguage(): PrinterCommandLanguage {
- return this._commandLanugage;
- }
- get dpi(): number {
- return this._dpi;
- }
- get model(): PrinterModel | string {
- return this._model;
- }
- get speedTable(): ReadonlyMap {
- return this._speedTable;
- }
- get maxDarkness(): number {
- return this._maxDarkness;
- }
-
- constructor(
- private _commandLanugage: PrinterCommandLanguage,
- private _dpi: number,
- private _model: PrinterModel | string,
- private _speedTable: ReadonlyMap,
- private _maxDarkness: number
- ) {
- super();
- }
+ get commandLanguage(): PrinterCommandLanguage {
+ return this._commandLanugage;
+ }
+ get dpi(): number {
+ return this._dpi;
+ }
+ get model(): PrinterModel | string {
+ return this._model;
+ }
+ get speedTable(): ReadonlyMap {
+ return this._speedTable;
+ }
+ get maxDarkness(): number {
+ return this._maxDarkness;
+ }
+
+ constructor(
+ private _commandLanugage: PrinterCommandLanguage,
+ private _dpi: number,
+ private _model: PrinterModel | string,
+ private _speedTable: ReadonlyMap,
+ private _maxDarkness: number
+ ) {
+ super();
+ }
}
diff --git a/src/Printers/Models/PrinterModelDb.ts b/src/Printers/Models/PrinterModelDb.ts
index 1921c0d..544c512 100644
--- a/src/Printers/Models/PrinterModelDb.ts
+++ b/src/Printers/Models/PrinterModelDb.ts
@@ -1,100 +1,103 @@
import { PrinterModel } from './PrinterModel.js';
import { PrinterCommandLanguage } from '../Configuration/PrinterOptions.js';
import * as EPL from './EplPrinterModels.js';
-import { IPrinterModelInfo, UnknownPrinter } from './PrinterModel.js';
+import { type IPrinterModelInfo, UnknownPrinter } from './PrinterModel.js';
+import type { IDeviceInformation } from '../Communication/DeviceCommunication.js';
export class PrinterModelDb {
- /** Determine a printer model based on the printer-reported model. */
- public static getModel(rawModelId: string): PrinterModel {
- if (rawModelId == null) {
- return PrinterModel.unknown;
- }
- // Easy mode: if it ends in FDX it's a fedex LP2844
- if (rawModelId.endsWith('FDX')) {
- return PrinterModel.lp2844fedex;
- }
- if (rawModelId.endsWith('UPS')) {
- return PrinterModel.lp2844ups;
- }
+ /** Determine a printer model based on the printer-reported model. */
+ public static getModel(rawModelId?: string): PrinterModel {
+ if (rawModelId === undefined) {
+ return PrinterModel.unknown;
+ }
+ // Easy mode: if it ends in FDX it's a fedex LP2844
+ if (rawModelId.endsWith('FDX')) {
+ return PrinterModel.lp2844fedex;
+ }
+ if (rawModelId.endsWith('UPS')) {
+ return PrinterModel.lp2844ups;
+ }
- // Hard mode: Model correlation between observed values and output.
- // This is pretty much all based off of observed values, I can't find a mapping
- // of the config's model number vs the hardware model number.
- // TODO: Make this extensible so it's possible for consumers to add their own
- // printers to the enum, match list, etc.
- switch (rawModelId) {
- case 'UKQ1915 U':
- // TODO: This is an educated guess, validate it!
- return PrinterModel.tlp2824;
- case 'UKQ1935 U':
- return PrinterModel.tlp2844;
+ // Hard mode: Model correlation between observed values and output.
+ // This is pretty much all based off of observed values, I can't find a mapping
+ // of the config's model number vs the hardware model number.
+ // TODO: Make this extensible so it's possible for consumers to add their own
+ // printers to the enum, match list, etc.
+ switch (rawModelId) {
+ case 'UKQ1915 U':
+ // TODO: This is an educated guess, validate it!
+ return PrinterModel.tlp2824;
+ case 'UKQ1935 U':
+ return PrinterModel.tlp2844;
- case 'UKQ1915HLU':
- return PrinterModel.lp2824;
- case 'UKQ1935HLU':
- return PrinterModel.lp2844;
- case 'UKQ1935HMU':
- // HMU units that do not have FDX in the version string appear to be UPS
- // units. Maybe. Mostly. It's not clear.
- return PrinterModel.lp2844ups;
+ case 'UKQ1915HLU':
+ return PrinterModel.lp2824;
+ case 'UKQ1935HLU':
+ return PrinterModel.lp2844;
+ case 'UKQ1935HMU':
+ // HMU units that do not have FDX in the version string appear to be UPS
+ // units. Maybe. Mostly. It's not clear.
+ return PrinterModel.lp2844ups;
- case 'LP2824-Z-200dpi':
- return PrinterModel.lp2824z;
- case 'LP2844-Z-200dpi':
- return PrinterModel.lp2844z;
- default:
- return PrinterModel.unknown;
- }
+ case 'ZPL_AUTODETECT':
+ return PrinterModel.zplAutodetect;
+ case 'LP2824-Z-200dpi':
+ return PrinterModel.lp2824z;
+ case 'LP2844-Z-200dpi':
+ return PrinterModel.lp2844z;
+ default:
+ return PrinterModel.unknown;
}
+ }
- /** Look up the model information for a given printer model. */
- public static getModelInfo(model: PrinterModel): IPrinterModelInfo {
- // TODO: Make this extensible so it's possible for consumers to add their own
- // printers to the enum, match list, etc.
- switch (model) {
- // LP models, direct thermal only.
- case PrinterModel.lp2824:
- return new EPL.LP2824();
- case PrinterModel.lp2844:
- case PrinterModel.lp2844fedex:
- case PrinterModel.lp2844ups:
- return new EPL.LP2844();
+ /** Look up the model information for a given printer model. */
+ public static getModelInfo(model: PrinterModel): IPrinterModelInfo {
+ // TODO: Make this extensible so it's possible for consumers to add their own
+ // printers to the enum, match list, etc.
+ switch (model) {
+ // LP models, direct thermal only.
+ case PrinterModel.lp2824:
+ return new EPL.LP2824();
+ case PrinterModel.lp2844:
+ case PrinterModel.lp2844fedex:
+ case PrinterModel.lp2844ups:
+ return new EPL.LP2844();
- // TLP models, direct thermal or thermal transfer.
- case PrinterModel.tlp2824:
- return new EPL.TLP2824();
- case PrinterModel.tlp2844:
- return new EPL.TLP2844();
+ // TLP models, direct thermal or thermal transfer.
+ case PrinterModel.tlp2824:
+ return new EPL.TLP2824();
+ case PrinterModel.tlp2844:
+ return new EPL.TLP2844();
- default:
- return new UnknownPrinter();
- }
+ default:
+ return new UnknownPrinter();
}
+ }
- public static guessLanguageFromModelHint(modelHint?: string): PrinterCommandLanguage {
- if (!modelHint) {
- return PrinterCommandLanguage.none;
- }
+ public static guessLanguageFromModelHint(deviceInfo?: IDeviceInformation): PrinterCommandLanguage {
+ if (deviceInfo === undefined) { return PrinterCommandLanguage.none; }
- // ZPL printers tend to be more trustworthy. They will follow a more standard
- // format.
- switch (true) {
- // LP2844
- // ZTC LP2844-Z-200dpi
- case /\sLP2844-Z-200dpi/gim.test(modelHint):
- return PrinterCommandLanguage.zplEmulateEpl;
- default:
- return PrinterCommandLanguage.none;
- }
+ const modelName = deviceInfo.productName ?? '';
+ // ZPL printers tend to be more trustworthy. They will follow a more standard
+ // format.
+ switch (true) {
+ // LP2844-Z
+ // ZTC LP2844-Z-200dpi
+ case /\sLP2844-Z-200dpi/gim.test(modelName):
+ case /\sLP2824-Z-200dpi/gim.test(modelName):
+ return PrinterCommandLanguage.zplEmulateEpl;
+ default:
+ return PrinterCommandLanguage.none;
+ }
- // EPL printers are all over the place. They range from blank to straight up lies.
- // I have an LP 2844 that claims to be a TPL2844 (it is not).
- // I have a FedEx unit that is blank.
- // I have a UPS unit that says UPS. I have another one that doesn't.
- // EPL printer model hints are not to be trusted.
+ // EPL printers are all over the place. They range from blank to straight up lies.
+ // I have an LP 2844 that claims to be a TPL2844 (it is not).
+ // I have a FedEx unit that is blank.
+ // I have a UPS unit that says UPS. I have another one that doesn't.
+ // EPL printer model hints are not to be trusted.
- // I don't have a CPCL printer to test and see what it might say. Someday I
- // may get my hands on one to test. If you'd like me to try one out contact me!
- // I'll be happy to discuss sending one to me to test and implement then send back.
- }
+ // I don't have a CPCL printer to test and see what it might say. Someday I
+ // may get my hands on one to test. If you'd like me to try one out contact me!
+ // I'll be happy to discuss sending one to me to test and implement then send back.
+ }
}
diff --git a/src/Printers/Printer.ts b/src/Printers/Printer.ts
index 87ac7ac..1874e10 100644
--- a/src/Printers/Printer.ts
+++ b/src/Printers/Printer.ts
@@ -1,262 +1,219 @@
-import { IDocument } from '../Documents/Document.js';
-import { ConfigDocumentBuilder, IConfigDocumentBuilder } from '../Documents/ConfigDocument.js';
+import { type IDocument } from '../Documents/Document.js';
+import { ConfigDocumentBuilder, type IConfigDocumentBuilder } from '../Documents/ConfigDocument.js';
import {
- ILabelDocumentBuilder,
- LabelDocumentBuilder,
- LabelDocumentType
+ type ILabelDocumentBuilder,
+ LabelDocumentBuilder,
+ LabelDocumentType
} from '../Documents/LabelDocument.js';
import { ReadyToPrintDocuments } from '../Documents/ReadyToPrintDocuments.js';
import { WebZlpError } from '../WebZlpError.js';
-import { IPrinterDeviceChannel, PrinterChannelType } from './Communication/PrinterCommunication.js';
-import { UsbPrinterDeviceChannel } from './Communication/UsbPrinterDeviceChannel.js';
+import { UsbDeviceChannel } from './Communication/UsbPrinterDeviceChannel.js';
import { PrinterCommandLanguage, PrinterOptions } from './Configuration/PrinterOptions.js';
import { EplPrinterCommandSet } from './Languages/EplPrinterCommandSet.js';
import { PrinterCommandSet } from './Languages/PrinterCommandSet.js';
import { ZplPrinterCommandSet } from './Languages/ZplPrinterCommandSet.js';
import { PrinterModelDb } from './Models/PrinterModelDb.js';
-import { PrinterCommunicationOptions } from './PrinterCommunicationOptions.js';
+import { DeviceNotReadyError, type IDeviceChannel, type IDeviceCommunicationOptions, DeviceCommunicationError, type IDeviceInformation, type IDevice } from './Communication/DeviceCommunication.js';
/**
* A class for working with a label printer.
*/
-export class Printer {
- // Printer communication handles
- private channelType: PrinterChannelType;
- private device: USBDevice;
- private printerChannel: IPrinterDeviceChannel;
- private commandset: PrinterCommandSet;
-
- private _printerConfig: PrinterOptions;
-
- private _ready: Promise;
- /** A promise indicating this printer is ready to be used. */
- get ready() {
- return this._ready;
- }
-
- /** Gets the model of the printer, detected from the printer's config. */
- get printerModel() {
- return this._printerConfig.model;
- }
-
- /** Gets the read-only copy of the current config of the printer. To modfiy use getConfigDocument. */
- get printerConfig() {
- return this._printerConfig.copy();
- }
-
- private _printerCommunicationOptions: PrinterCommunicationOptions;
- /** Gets the configured printer communication options. */
- get printerCommunicationOptions() {
- return this._printerCommunicationOptions;
- }
-
- /** Construct a new printer from a given USB device. */
- static fromUSBDevice(device: USBDevice, options?: PrinterCommunicationOptions): Printer {
- return new this(PrinterChannelType.usb, device, options);
- }
-
- constructor(
- channelType: PrinterChannelType,
- device: USBDevice,
- options?: PrinterCommunicationOptions
- ) {
- this.channelType = channelType;
- this._printerCommunicationOptions = options ?? new PrinterCommunicationOptions();
-
- switch (this.channelType) {
- case PrinterChannelType.usb:
- this.device = device;
- this.printerChannel = new UsbPrinterDeviceChannel(
- this.device,
- this._printerCommunicationOptions.debug
- );
- break;
- case PrinterChannelType.serial:
- case PrinterChannelType.bluetooth:
- case PrinterChannelType.network:
- throw new WebZlpError('Printer comm method not implemented.');
- }
-
- this._ready = this.setup();
- }
-
- private async setup() {
- await this.printerChannel.ready;
- await this.refreshPrinterConfiguration(this.printerChannel.modelHint);
- return true;
- }
-
- /** Gets a document for configuring this printer. */
- public getConfigDocument(): IConfigDocumentBuilder {
- return new ConfigDocumentBuilder(this._printerConfig);
- }
-
- /** Gets a document for printing a label. */
- public getLabelDocument(
- docType: LabelDocumentType = LabelDocumentType.instanceForm
- ): ILabelDocumentBuilder {
- return new LabelDocumentBuilder(this._printerConfig, docType);
- }
-
- /** Send a document to the printer, applying the commands. */
- public async sendDocument(doc: IDocument) {
- await this.ready;
-
- if (this._printerCommunicationOptions.debug) {
- console.debug('SENDING COMMANDS TO PRINTER:');
- console.debug(doc.showCommands());
- }
-
- // Exceptions are thrown and handled elsewhere.
- const compiled = this.commandset.transpileDoc(doc);
-
- if (this._printerCommunicationOptions.debug) {
- console.debug('RAW COMMAND BUFFER:');
- console.debug(compiled.commandBufferString);
- }
-
- await this.printerChannel.sendCommands(compiled.commandBuffer);
- }
-
- /** Close the connection to this printer, preventing future communication from working. */
- public async dispose() {
- await this.printerChannel.dispose();
+export class LabelPrinter implements IDevice {
+ // Printer communication handles
+ private _channel: IDeviceChannel;
+ private _commandSet?: PrinterCommandSet;
+
+ private _printerOptions: PrinterOptions;
+ /** Gets the read-only copy of the current config of the printer. To modify use getConfigDocument. */
+ get printerOptions() { return this._printerOptions.copy(); }
+ /** Gets the model of the printer, detected from the printer's config. */
+ get printerModel() { return this._printerOptions.model; }
+ /** Gets the serial number of the printer, detected from the printer's config. */
+ get printerSerial() { return this._printerOptions.serialNumber; }
+
+ private _deviceCommOpts: IDeviceCommunicationOptions;
+ /** Gets the configured printer communication options. */
+ get printerCommunicationOptions() {
+ return this._deviceCommOpts;
+ }
+
+ private _disposed = false;
+ private _ready: Promise;
+ /** A promise indicating this printer is ready to be used. */
+ get ready() {
+ return this._ready;
+ }
+ get connected() {
+ return !this._disposed
+ && this._channel.connected
+ }
+
+ /** Construct a new printer from a given USB device. */
+ static fromUSBDevice(
+ device: USBDevice,
+ options: IDeviceCommunicationOptions
+ ): LabelPrinter {
+ return new LabelPrinter(new UsbDeviceChannel(device, options), options);
+ }
+
+ constructor(
+ channel: IDeviceChannel,
+ deviceCommunicationOptions: IDeviceCommunicationOptions = { debug: false },
+ printerOptions?: PrinterOptions,
+ ) {
+ this._channel = channel;
+ this._deviceCommOpts = deviceCommunicationOptions;
+ this._printerOptions = printerOptions ?? PrinterOptions.invalid;
+ this._ready = this.setup();
+ }
+
+ private async setup() {
+ const channelReady = await this._channel.ready;
+ if (!channelReady) {
+ // If the channel failed to connect we have no hope.
+ return false;
+ }
+
+ await this.refreshPrinterConfiguration(this._channel.getDeviceInfo());
+ return true;
+ }
+
+ /** Gets a document for configuring this printer. */
+ public getConfigDocument(): IConfigDocumentBuilder {
+ return new ConfigDocumentBuilder(this._printerOptions);
+ }
+
+ /** Gets a document for printing a label. */
+ public getLabelDocument(
+ docType: LabelDocumentType = LabelDocumentType.instanceForm
+ ): ILabelDocumentBuilder {
+ return new LabelDocumentBuilder(this._printerOptions, docType);
+ }
+
+ /** Send a document to the printer, applying the commands. */
+ public async sendDocument(doc: IDocument) {
+ await this.ready;
+ if (!this.connected || this._commandSet === undefined) {
+ throw new DeviceNotReadyError("Printer is not ready to communicate.");
+ }
+
+ if (this._deviceCommOpts.debug) {
+ console.debug('SENDING COMMANDS TO PRINTER:');
+ console.debug(doc.showCommands());
+ }
+
+ // Exceptions are thrown and handled elsewhere.
+ const compiled = this._commandSet.transpileDoc(doc);
+
+ if (this._deviceCommOpts.debug) {
+ console.debug('RAW COMMAND BUFFER:');
+ console.debug(compiled.commandBufferString);
+ }
+
+ await this._channel.sendCommands(compiled.commandBuffer);
+ }
+
+ /** Close the connection to this printer, preventing future communication. */
+ public async dispose() {
+ this._disposed = true;
+ this._ready = Promise.resolve(false);
+ await this._channel.dispose();
+ }
+
+ /** Refresh the printer information cache directly from the printer. */
+ public async refreshPrinterConfiguration(deviceInfo?: IDeviceInformation): Promise {
+ if (!this._printerOptions.valid) {
+ // First time pulling the config. Detect language and model.
+ this._printerOptions = await this.detectLanguageAndSetConfig(deviceInfo);
+ } else {
+ this._printerOptions = await this.tryGetConfig(this._commandSet);
+ }
+
+ if (!this._printerOptions.valid) {
+ throw new WebZlpError(
+ 'Failed to detect the printer information, either the printer is unknown or the config can not be parsed. This printer can not be used.'
+ );
+ }
+ return this._printerOptions;
+ }
+
+ private async detectLanguageAndSetConfig(deviceInfo?: IDeviceInformation): Promise {
+ const guess = PrinterModelDb.guessLanguageFromModelHint(deviceInfo);
+ // Guess order is easiest to detect and support.. to least
+ const guessOrder = [
+ guess,
+ PrinterCommandLanguage.epl,
+ PrinterCommandLanguage.zpl,
+ PrinterCommandLanguage.cpcl
+ ];
+
+ // For each language, we send the appropriate command to try and get the
+ // config dump. If we get something legible back break out.
+ for (let i = 0; i < guessOrder.length; i++) {
+ const set = this.getCommandSetForLanguage(guessOrder[i]);
+ if (set === undefined) {
+ continue;
+ }
+ this.logIfDebug('Trying printer language guess', PrinterCommandLanguage[guessOrder[i]]);
+ const config = await this.tryGetConfig(set);
+ if (config.valid) {
+ this._commandSet = set;
+ return config;
+ }
}
- /** Refresh the printer information cache directly from the printer. */
- public async refreshPrinterConfiguration(modelHint?: string): Promise {
- if (!this._printerConfig) {
- // First time pulling the config. Detect language and model.
- this._printerConfig = await this.detectLanguageAndSetConfig(modelHint);
- } else {
- this._printerConfig = await this.tryGetConfig(this.commandset);
- }
+ return { valid: false } as PrinterOptions;
+ }
- if (!this._printerConfig?.valid) {
- throw new WebZlpError(
- 'Failed to detect the printer information, either the printer is unknown or the config can not be parsed. This printer can not be used.'
- );
- }
- return this._printerConfig;
+ private getCommandSetForLanguage(lang: PrinterCommandLanguage): PrinterCommandSet | undefined {
+ // In order of preferred communication method
+ if (PrinterCommandLanguage.zpl === (lang & PrinterCommandLanguage.zpl)) {
+ return new ZplPrinterCommandSet();
}
-
- private async detectLanguageAndSetConfig(modelHint?: string): Promise {
- const guess = PrinterModelDb.guessLanguageFromModelHint(modelHint);
- // Guess order is easiest to detect and support.. to least
- const guessOrder = [
- guess,
- PrinterCommandLanguage.epl,
- PrinterCommandLanguage.zpl,
- PrinterCommandLanguage.cpcl
- ];
-
- // For each language, we send the appropriate command to try and get the
- // config dump. If we get something legible back break out.
- for (let i = 0; i < guessOrder.length; i++) {
- const set = this.getCommandSetForLanguage(guessOrder[i]);
- if (set == null) {
- continue;
- }
- this.logIfDebug('Trying printer language guess', PrinterCommandLanguage[guessOrder[i]]);
- const config = await this.tryGetConfig(set);
- if (config.valid) {
- this.commandset = set;
- return config;
- }
- }
-
- return { valid: false } as PrinterOptions;
+ if (PrinterCommandLanguage.epl === (lang & PrinterCommandLanguage.epl)) {
+ return new EplPrinterCommandSet();
}
+ return undefined;
+ }
- private getCommandSetForLanguage(lang: PrinterCommandLanguage): PrinterCommandSet {
- // In order of preferred communication method
- if (PrinterCommandLanguage.zpl === (lang & PrinterCommandLanguage.zpl)) {
- return new ZplPrinterCommandSet();
- }
- if (PrinterCommandLanguage.epl === (lang & PrinterCommandLanguage.epl)) {
- return new EplPrinterCommandSet();
- }
- return null;
- }
+ private async tryGetConfig(cmdSet?: PrinterCommandSet): Promise {
+ let config = PrinterOptions.invalid;
+ if (cmdSet === undefined) { return config; }
- private async tryGetConfig(cmdSet: PrinterCommandSet): Promise {
- // TODO: Move this elsewhere so we don't create a new one each time.
- // Safe to use a raw document with null metadata since the data isn't used here.
- const compiled = cmdSet.transpileDoc(ReadyToPrintDocuments.configDocument);
- this.logIfDebug('Querying printer config with', compiled.commandBufferString);
+ const compiled = cmdSet.transpileDoc(ReadyToPrintDocuments.configDocument);
+ this.logIfDebug('Querying printer config with', compiled.commandBufferString);
- let config: PrinterOptions;
- // Querying for a config doesn't always.. work? Like, just straight up
- // for reasons I can't figure out some printers will refuse to return
- // a valid config. Mostly EPL models.
- // Give it 3 chances before we give up.
- let retryLimit = 3;
- do {
- retryLimit--;
+ // Querying for a config doesn't always.. work? Like, just straight up
+ // for reasons I can't figure out some printers will refuse to return
+ // a valid config. Mostly EPL models.
+ // Give it 3 chances before we give up.
+ let retryLimit = 3;
+ do {
+ retryLimit--;
- // Start listening for the return from the
- const listenEpl = this.listenForData();
+ // Start listening for the return from the printer
+ const awaitInput = this._channel.getInput(); // this.listenForData();
- // Config isn't set up yet, send command directly without the send command.
- await this.printerChannel.sendCommands(compiled.commandBuffer);
- const rawResult = await listenEpl;
- config = cmdSet.parseConfigurationResponse(
- rawResult,
- this._printerCommunicationOptions
- );
- } while (!config.valid && retryLimit > 0);
+ // Config isn't set up yet, send command directly without the send command.
+ await this._channel.sendCommands(compiled.commandBuffer);
+ const rawResult = await awaitInput;
+ if (rawResult instanceof DeviceCommunicationError) {
+ continue;
+ }
+ config = cmdSet.parseConfigurationResponse(
+ rawResult.join(),
+ this._printerOptions
+ );
+ } while (!config.valid && retryLimit > 0);
- this.logIfDebug(`Config result is ${config.valid ? 'valid' : 'not valid.'}`);
+ this.logIfDebug(`Config result is ${config.valid ? 'valid' : 'not valid.'}`);
- return config;
- }
-
- /** Wait for the next line of data sent from the printer, or undefined if nothing is received. */
- private async nextLine(timeoutMs: number): Promise {
- let reader: ReadableStreamDefaultReader;
- const nextLinePromise = (async () => {
- reader = this.printerChannel.streamFromPrinter.getReader();
- const { value, done } = await reader.read();
- reader.releaseLock();
-
- if (done) {
- return;
- }
-
- return value;
- })();
-
- const timeoutPromise = new Promise((resolve) => {
- setTimeout(() => {
- reader.releaseLock();
- resolve();
- }, timeoutMs);
- });
-
- return Promise.race([nextLinePromise, timeoutPromise]);
- }
-
- /** Listen for incoming data until a timeout, assuming the source is done. */
- private async listenForData(timeoutMs = 300) {
- let aggregate = '';
- for (;;) {
- const line = await this.nextLine(timeoutMs);
- if (line === undefined) {
- this.logIfDebug(
- 'Received',
- aggregate.length,
- 'long message from printer:\n',
- aggregate
- );
- return aggregate;
- }
- aggregate += line + '\n';
- }
- }
+ return config;
+ }
- private logIfDebug(...obj: unknown[]) {
- if (this._printerCommunicationOptions.debug) {
- console.debug(...obj);
- }
+ private logIfDebug(...obj: unknown[]) {
+ if (this._deviceCommOpts.debug) {
+ console.debug(...obj);
}
+ }
}
diff --git a/src/Printers/PrinterCommunicationOptions.ts b/src/Printers/PrinterCommunicationOptions.ts
deleted file mode 100644
index de606ce..0000000
--- a/src/Printers/PrinterCommunicationOptions.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { PrinterCommandLanguage } from './Configuration/PrinterOptions.js';
-import {
- CommandFormInclusionMode,
- TranspileCommandDelegate
-} from './Languages/PrinterCommandSet.js';
-
-export class PrinterCommunicationOptions {
- /**
- * Value to use for rounding read-from-config label sizes.
- *
- * When reading the config from a printer the label width and height may be
- * variable. When you set the label width to 4 inches it's translated into
- * dots, and then the printer adds a calculated offset to that. This offset
- * is unique per printer (so far as I have observed) and introduces noise.
- * This value rounds the returned value to the nearest fraction of an inch.
- *
- * For example, with a rounding step of 0.25 (the default) if the printer
- * returns a width 4.113 it will be rounded to 4.0
- */
- public labelDimensionRoundingStep = 0.25;
-
- /**
- * Whether to display printer communication to the dev console
- */
- public debug = false;
-
- /**
- * Custom printer commands added to the base set for a given language.
- *
- * See the documentation for more details on how to implement this.
- */
- public additionalCustomCommands: Array<{
- commandType: symbol;
- applicableLanguages: PrinterCommandLanguage;
- transpileDelegate: TranspileCommandDelegate;
- commandInclusionMode: CommandFormInclusionMode;
- }> = null;
-}
diff --git a/src/Printers/index.ts b/src/Printers/index.ts
new file mode 100644
index 0000000..d2a8686
--- /dev/null
+++ b/src/Printers/index.ts
@@ -0,0 +1,13 @@
+export * from './Communication/DeviceCommunication.js'
+export * from './Communication/LineBreakTransformer.js'
+export * from './Communication/UsbPrinterDeviceChannel.js'
+export * from './Configuration/MediaOptions.js'
+export * from './Configuration/PrinterOptions.js'
+export * from './Configuration/SerialPortSettings.js'
+export * from './Languages/EplPrinterCommandSet.js'
+export * from './Languages/PrinterCommandSet.js'
+export * from './Languages/ZplPrinterCommandSet.js'
+export * from './Models/EplPrinterModels.js'
+export * from './Models/PrinterModel.js'
+export * from './Models/PrinterModelDb.js'
+export * from './Printer.js'
diff --git a/src/WebZlpError.ts b/src/WebZlpError.ts
index f92fa70..8a96c26 100644
--- a/src/WebZlpError.ts
+++ b/src/WebZlpError.ts
@@ -1,7 +1,7 @@
/** Exception thrown from the WebZLP library. */
export class WebZlpError extends Error {
- constructor(message: string) {
- super(message);
- this.name = this.constructor.name;
- }
+ constructor(message: string) {
+ super(message);
+ this.name = this.constructor.name;
+ }
}
diff --git a/src/index.ts b/src/index.ts
index 66936e5..1465698 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,20 +1,5 @@
-export * from './Color.js';
-export * from './Documents/Commands.js';
-export * from './Documents/ConfigDocument.js';
-export * from './Documents/Document.js';
-export * from './Documents/BitmapGRF.js';
-export * from './Documents/LabelDocument.js';
-export * from './Documents/ReadyToPrintDocuments.js';
-export * from './Printers/Communication/LineBreakTransformer.js';
-export * from './Printers/Communication/PrinterCommunication.js';
-export * from './Printers/Communication/UsbPrinterDeviceChannel.js';
-export * from './Printers/Configuration/PrinterOptions.js';
-export * from './Printers/Languages/EplPrinterCommandSet.js';
-export * from './Printers/Languages/PrinterCommandSet.js';
-export * from './Printers/Models/EplPrinterModels.js';
-export * from './Printers/Models/PrinterModel.js';
-export * from './Printers/Models/PrinterModelDb.js';
-export * from './Printers/PrinterCommunicationOptions.js';
-export * from './Printers/Printer.js';
+export * from './Documents/index.js';
+export * from './Printers/index.js';
+export * from './NumericRange.js';
export * from './PrinterUsbManager.js';
export * from './WebZlpError.js';
diff --git a/tsconfig.json b/tsconfig.json
index e5058cc..cd935e2 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,12 +1,28 @@
{
"compilerOptions": {
- "module": "es6",
- "types": [],
- "lib": ["dom", "esnext"],
- "outDir": "./dist/",
- "sourceMap": true,
- "target": "esnext",
+ "target": "ES2022",
+ "module": "nodenext",
+ "moduleResolution": "nodenext",
+ "lib": [
+ "ESNext",
+ "DOM",
+ "DOM.Iterable"
+ ],
+ "skipLibCheck": false,
+ "isolatedModules": true,
+ "noEmit": true,
+ "strict": true,
+ "checkJs": true,
"declaration": true,
- "moduleResolution": "node"
- }
+ "declarationMap": true,
+ "noUnusedLocals": true,
+ "useDefineForClassFields": true,
+ "noUnusedParameters": true,
+ "forceConsistentCasingInFileNames": true,
+ "verbatimModuleSyntax": true,
+ "incremental": true
+ },
+ "include": [
+ "src"
+ ]
}
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..05ab012
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,25 @@
+///
+import path from 'path';
+import packageJson from './package.json';
+import { defineConfig } from 'vite';
+import dts from 'vite-plugin-dts';
+import eslint from 'vite-plugin-eslint';
+
+export default defineConfig({
+ build: {
+ lib: {
+ entry: path.resolve(__dirname, 'src/index.ts'),
+ name: packageJson.name,
+ },
+ minify: false,
+ },
+ plugins: [dts(), eslint({
+ failOnError: false
+ })],
+ test: {
+ coverage: {
+ reporter: ['text', 'json-summary', 'json'],
+ reportOnFailure: true,
+ }
+ }
+});