escopi
v1.0.0
Published
Escopi converts raw ESC/POS data into a PDF that can be previewed, printed, downloaded, uploaded, or stored through standard browser APIs.
Maintainers
Readme
Escopi converts raw ESC/POS data into a PDF that can be previewed, printed, downloaded, uploaded, or stored through standard browser APIs. It does not require a backend PDF service or a custom PDF viewer.
Features
- Converts ESC/POS data into PDF files directly in the browser
- Accepts Base64, Base64 data URLs,
Uint8Array,ArrayBuffer,DataView, and otherArrayBufferviews - Returns the generated PDF as a
Uint8Array,Blob, and optional temporary Blob URL - Works with the browser's native PDF viewer
- Includes built-in 58 mm and 80 mm paper profiles
- Automatically paginates long tickets
- Renders formatted text, raster images, QR codes, and one-dimensional barcodes
- Supports multiple code pages and international character substitutions
- Reports unsupported, malformed, and intentionally ignored commands
Demo
A demo is available at https://escopi-demo.vercel.app/
Installation
npm
npm install escopiimport Escopi from "escopi";Named exports are also available:
import {
toPdf,
parse,
renderToPages,
FONT_OPTIONS,
EscopiRenderError
} from "escopi";CDN
<script src="https://cdn.jsdelivr.net/npm/escopi@1/dist/escopi.min.js"></script>The CDN bundle exposes the global Escopi object. toPdf(), renderToPages(), and renderToCanvas() are all asynchronous functions.
The default Departure Mono font is embedded inside each distributed JavaScript bundle. A consuming page only needs the Escopi JavaScript file; no separate font file or font stylesheet is required.
How does Escopi work?
ESC/POS input
-> byte normalization
-> ESC/POS parsing
-> intermediate elements
-> asynchronous font resolution
-> paginated canvas rendering
-> one-bit monochrome image encoding
-> native PDF 1.4 structure
-> Uint8Array + Blob + optional Blob URL- Input normalization: Base64, buffers, and
ArrayBufferviews are converted into aUint8Array. - Parsing: ESC/POS control sequences are converted into line, image, barcode, QR, feed, cut, and action elements.
- Font resolution:
font: "departure-mono"loads the Departure Mono font embedded in the JavaScript bundle. A failed load falls back tomonospace.font: "monospace"skips the embedded-font load. - Layout: Elements are positioned using the resolved paper width, printable area, font cells, margins, alignment, and pagination rules.
- QR and barcode encoding: The bundled
qrcode-generatorandJsBarcodepackages produce the QR matrix and barcode module pattern. They are included in the distributed bundles and are not downloaded at runtime. - Canvas rendering: Every PDF page is first rendered to an
HTMLCanvasElementorOffscreenCanvas.renderToCanvas()can additionally stack those page canvases into one continuous canvas. - Monochrome conversion: Canvas pixels are converted into a one-bit grayscale image using
pdfThreshold. - PDF generation: Escopi writes a PDF 1.4 document with one image XObject per page. Image data uses Deflate compression when
CompressionStreamis available and remains uncompressed otherwise. - Browser output: The PDF bytes are wrapped in an
application/pdfBlob, and a Blob URL is optionally created.
Quick start
<iframe
id="viewer"
title="Ticket preview"
style="width:100%;height:700px;border:0"
></iframe>
<script src="https://cdn.jsdelivr.net/npm/escopi@1/dist/escopi.min.js"></script>
<script>
let currentResult = null;
async function showTicket(base64EscPos) {
if (currentResult !== null) {
Escopi.revoke(currentResult);
}
currentResult = await Escopi.toPdf(base64EscPos, {
profile: "80mm"
});
document.getElementById("viewer").src = currentResult.url;
}
</script>Escopi does not include a custom PDF viewer. The browser's native viewer provides its own zoom, print, and download controls.
Accepted input formats
Escopi.toPdf() and Escopi.parse() accept the following input values:
| Input | Example | Behavior |
| --- | --- | --- |
| Base64 string | "G0AbYQ..." | Whitespace is removed before decoding |
| Base64 data URL | "data:application/octet-stream;base64,G0AbYQ..." | Everything before the first comma is removed |
| Uint8Array | new Uint8Array([...]) | Used directly |
| ArrayBuffer | await response.arrayBuffer() | Wrapped in a Uint8Array |
| DataView | new DataView(buffer) | Its underlying byte range is used |
| Other ArrayBuffer view | new Uint16Array(buffer) | Its raw underlying bytes are used |
Every JavaScript string is interpreted as Base64. Raw binary strings and ordinary text strings are not accepted as ESC/POS input.
Base64 input
const result = await Escopi.toPdf(base64EscPos, {
profile: "80mm"
});Uint8Array input
const escposBytes = new Uint8Array([
27, 64,
27, 97, 1,
72, 101, 108, 108, 111,
10
]);
const result = await Escopi.toPdf(escposBytes);ArrayBuffer input
const response = await fetch("/api/ticket");
const escposBuffer = await response.arrayBuffer();
const result = await Escopi.toPdf(escposBuffer);Working with the generated PDF
Show it in an iframe
const result = await Escopi.toPdf(input);
document.getElementById("viewer").src = result.url;Show it in an <object>
<object
id="viewer"
type="application/pdf"
width="100%"
height="700"
></object>const result = await Escopi.toPdf(input);
document.getElementById("viewer").data = result.url;Open it in a new tab
const result = await Escopi.toPdf(input);
window.open(result.url, "_blank", "noopener,noreferrer");Browsers may block window.open() unless it is called from a user gesture such as a button click.
Download it
const result = await Escopi.toPdf(input);
Escopi.download(result, "ticket.pdf");Convert the PDF to Base64
const result = await Escopi.toPdf(input);
const pdfBase64 = await Escopi.toBase64(result);Escopi.toBase64() also accepts a Blob, Uint8Array, or ArrayBuffer.
Release the Blob URL
Escopi.revoke(result);A Blob URL is a temporary browser reference to the in-memory PDF. It is not uploaded to the displayed origin. Revoke it when it is no longer needed.
Result object
Escopi.toPdf() resolves to an EscopiResult.
Runtime structure
The following block describes the returned types.
{
bytes: Uint8Array;
blob: Blob;
url: string | null;
meta: {
profile: string;
paperWidthMm: number;
printableWidthDots: number;
columns: {
fontA: number;
fontB: number;
};
font: {
requested: "departure-mono" | "monospace";
used: "departure-mono" | "monospace";
family: string;
loaded: boolean;
fallbackUsed: boolean;
};
pageCount: number;
pages: Array<{
number: number;
widthMm: number;
heightMm: number;
}>;
totalHeightMm: number;
escposByteCount: number;
elementCount: number;
diagnostics: EscopiDiagnostic[];
actions: EscopiActionElement[];
};
}Property reference
| Property | Runtime type | Description |
| --- | --- | --- |
| result.bytes | Uint8Array | Complete PDF file bytes |
| result.blob | Blob | PDF Blob with MIME type application/pdf |
| result.url | string \| null | Temporary Blob URL; null when disabled, unavailable, or revoked |
| meta.profile | string | Resolved profile name |
| meta.paperWidthMm | number | Physical width of each generated PDF page |
| meta.printableWidthDots | number | Actual printable width after profile resolution, paper-width clamping, and any marginMm override |
| meta.columns.fontA | number | Calculated Font A capacity using the resolved printable width and Font A cell width |
| meta.columns.fontB | number | Calculated Font B capacity using the resolved printable width and Font B cell width |
| meta.font | EscopiFontMetadata | Requested font, actual font, CSS family, load status, and fallback status |
| meta.pageCount | number | Number of generated PDF pages |
| meta.pages | EscopiPageMetadata[] | Number, width, and height of every generated page |
| meta.totalHeightMm | number | Sum of all generated page heights |
| meta.escposByteCount | number | Number of normalized ESC/POS input bytes |
| meta.elementCount | number | Number of top-level intermediate elements produced by the parser |
| meta.diagnostics | EscopiDiagnostic[] | Parser diagnostics |
| meta.actions | EscopiActionElement[] | Hardware-only actions extracted from the stream |
Inspect the result safely
const result = await Escopi.toPdf(input, {
profile: "80mm"
});
console.log(result.bytes instanceof Uint8Array);
console.log(result.bytes.byteLength);
console.log(result.blob instanceof Blob);
console.log(result.blob.size);
console.log(result.blob.type);
console.log(result.url);
console.log(result.meta);Example console values:
true
18432
true
18432
application/pdf
blob:https://example.com/936c9ce0-9b44-4535-a583-bbfe551b6972The byte count and Blob size depend on the ticket. Because the Blob is created directly from result.bytes, result.blob.size normally equals result.bytes.byteLength.
Blob URL behavior
result.url is null in any of the following cases:
createObjectUrlisfalseURL.createObjectURL()is unavailableEscopi.revoke(result)has already been called
const result = await Escopi.toPdf(input, {
createObjectUrl: false
});
console.log(result.url);nullHardware behavior
Escopi does not connect to external hardware.
| ESC/POS operation | Escopi behavior | Physical hardware effect |
| --- | --- | --- |
| ESC p m t1 t2 cash-drawer pulse | Produces an "action" element and adds it to result.meta.actions | None |
| GS V, ESC i, or ESC m cut | Produces a "cut" element; it can create a PDF page break or visible marker | None |
| ESC = n and ESC U n | Produces an "ignored-by-design" diagnostic | None |
| Unknown ESC/POS command | Diagnostic, omission, or exception according to unsupported | None |
A cash-drawer pulse is returned with the raw ESC/POS parameter bytes:
{
type: "action",
action: "cash-drawer",
data: {
pin: 0,
onTime: 50,
offTime: 100
}
}Escopi does not convert onTime or offTime to milliseconds. They remain the raw t1 and t2 values from the command.
A consuming application may forward the action to its own hardware layer:
const result = await Escopi.toPdf(input);
for (const action of result.meta.actions) {
if (action.action === "cash-drawer") {
await hardwareBridge.openCashDrawer(action.data);
}
}hardwareBridge is not part of Escopi.
Printing the generated PDF prints only the visual PDF. It does not resend the original ESC/POS stream and does not activate a cutter or cash drawer.
Paper profiles
Built-in profiles
| Profile | Paper width | Printable width | Dots per millimeter | Computed Font A columns | Computed Font B columns |
| --- | ---: | ---: | ---: | ---: | ---: |
| "58mm" | 58 mm | 384 dots | 8 | 32 | 42 |
| "80mm" | 80 mm | 576 dots | 8 | 48 | 64 |
The computed columns use the default cell widths:
- Font A: 12 dots
- Font B: 9 dots
Escopi.PROFILES stores dimensions and font metrics, not precomputed column counts.
Custom paper dimensions
const result = await Escopi.toPdf(input, {
profile: "80mm",
paperWidthMm: 76,
printableWidthDots: 560,
dotsPerMm: 8
});The public profile values are "58mm" and "80mm". Custom dimensions are supplied by overriding the numerical options.
Configuration
Paper and layout
| Option | Type | Default | Accepted values or behavior |
| --- | --- | --- | --- |
| profile | "58mm" \| "80mm" | "80mm" | Selects the built-in profile |
| paperWidthMm | number | Profile value | Must be finite and greater than 0 |
| printableWidthDots | number | Profile value | Must be finite and greater than 0; clamped to the physical paper width in pixels |
| dotsPerMm | number | 8 | Must be finite and greater than 0 |
| marginMm | number | Centered automatically | When finite, replaces printableWidthDots with the paper width minus two equal margins |
| verticalMarginMm | number | 3 | Negative values are clamped to 0 |
| font | "departure-mono" \| "monospace" | "departure-mono" | Uses the embedded Departure Mono font or browser monospace |
| fontAWidthDots | number | 12 | Font A cell width |
| fontAHeightDots | number | 24 | Font A cell height |
| fontBWidthDots | number | 9 | Font B cell width |
| fontBHeightDots | number | 17 | Font B cell height |
| defaultLineHeightDots | number | 30 | Default visual line and feed-line height |
| overflow | "wrap" \| "clip" \| "error" | "wrap" | Controls text that exceeds the printable width |
Ticket font
| Value | Behavior |
| --- | --- |
| "departure-mono" | Default. Escopi loads the embedded, unmodified Departure Mono font before rendering |
| "monospace" | Uses the browser's generic monospace font and skips embedded-font loading |
const result = await Escopi.toPdf(input, {
font: "departure-mono"
});PDF and output
| Option | Type | Default | Accepted values or behavior |
| --- | --- | --- | --- |
| createObjectUrl | boolean | true | When false, result.url is not created |
| pdfThreshold | number | 190 | Luminance threshold used when converting rendered pages to one-bit monochrome PDF images |
| showCut | boolean | false | Draws a dashed cut marker |
| cutAsPageBreak | boolean | true | Finishes the current PDF page after every parsed cut element |
QR codes, barcodes, and NV images
| Option | Type | Default | Accepted values or behavior |
| --- | --- | --- | --- |
| qrQuietZoneModules | number | 4 | Number of white modules around the generated QR matrix |
| barcodeMinModuleDots | number | 1 | Minimum permitted barcode module width |
| barcodeFit | "shrink" \| "error" | "shrink" | Shrinks an oversized barcode or throws an error |
| nvImages | RasterImageDefinition[] \| Record<number, RasterImageDefinition> | None | External NV-image registry used by FS p |
An array-based nvImages registry is numbered starting at 1. An object-based registry uses its numeric keys.
const result = await Escopi.toPdf(input, {
nvImages: {
1: {
data: logoBytes,
widthBytes: 48,
heightDots: 96
}
}
});A raster definition has the following public shape:
{
data: Uint8Array | number[];
widthBytes?: number;
widthDots?: number;
heightDots: number;
scaleX?: number;
scaleY?: number;
align?: "left" | "center" | "right";
}At least one usable width must be provided. The normalized data must contain at least widthBytes * heightDots bytes. When an external NV image is printed by FS p, the current ESC/POS alignment and the FS p print mode determine the emitted image element.
Parser behavior
| Option | Type | Default behavior | Accepted values |
| --- | --- | --- | --- |
| codepage | string | "cp850" | Any API code-page name listed under Code pages |
| unsupported | "diagnostic" \| "ignore" \| "error" | "diagnostic" | Controls unsupported commands |
| malformed | "diagnostic" \| "error" | "error" | Controls incomplete or invalid command data |
Limits
const result = await Escopi.toPdf(input, {
limits: {
maxInputBytes: 10_000_000,
maxElements: 50_000,
maxImageWidthDots: 4096,
maxImageHeightDots: 100_000,
maxQrBytes: 2953,
maxBarcodeLength: 500,
maxPageHeightMm: 1000,
maxPages: 100,
maxCanvasHeightPx: 8192
}
});| Limit | Default | Enforced behavior |
| --- | ---: | --- |
| maxInputBytes | 10,000,000 | Maximum normalized ESC/POS byte count |
| maxElements | 50,000 | Maximum number of top-level parsed elements |
| maxImageWidthDots | 4,096 | Maximum normalized raster width |
| maxImageHeightDots | 100,000 | Maximum normalized raster height |
| maxQrBytes | 2,953 | Maximum stored QR payload length |
| maxBarcodeLength | 500 | Maximum parsed barcode payload length |
| maxPageHeightMm | 1,000 | Maximum configured page height before the canvas limit is applied |
| maxPages | 100 | Maximum number of rendered pages |
| maxCanvasHeightPx | 8,192 | Maximum canvas height for each page |
Limit violations throw EscopiLimitError.
The effective maximum page height in pixels is the smaller of:
maxPageHeightMm * dotsPerMm
maxCanvasHeightPxSupported ESC/POS commands
Escopi implements a defined PDF-renderable subset of ESC/POS. It does not attempt to execute physical printer operations.
Control characters
| Command | Decimal byte | Behavior |
| --- | ---: | --- |
| HT | 9 | Adds a "tab" operation to the current line |
| LF | 10 | Finishes the current line, including an empty line |
| CR | 13 | Consumed without adding another line |
Other bytes below decimal 32 that are not recognized command prefixes are ignored.
ESC commands
| Command | Description | Parsed or rendered behavior |
| --- | --- | --- |
| ESC @ | Initialize | Resets the parser state to its defaults |
| ESC a n | Select alignment | 0/48: left, 1/49: center, 2/50: right; every other value becomes left |
| ESC ! n | Select print mode | Updates font, bold, double-height, double-width, and underline flags |
| ESC E n | Bold mode | Uses bit 0 to enable or disable bold |
| ESC G n | Double-strike mode | Uses bit 0 and is rendered through the same bold state |
| ESC - n | Underline mode | Stores underline thickness 0, 1, or 2 |
| ESC SP n | Character spacing | Stores n additional dots per character cell |
| ESC M n | Select font | Even values select Font A; odd values select Font B |
| ESC R n | International set | Accepts values 0 through 13; other values are unsupported |
| ESC t n | Select code page | Uses the implemented ESC t mapping table |
| ESC V n | Rotate text | Uses bit 0 to rotate subsequent glyphs by 90 degrees |
| ESC { n | Upside-down mode | Uses bit 0 to rotate subsequent glyphs by 180 degrees |
| ESC $ nL nH | Absolute position | Adds an absolute horizontal "position" operation |
| ESC \ nL nH | Relative position | Adds a signed 16-bit relative horizontal "position" operation |
| ESC D ... NUL | Set tab stops | Replaces the current tab-stop byte list |
| ESC 2 | Default line spacing | Restores the renderer's default line spacing |
| ESC 3 n | Custom line spacing | Stores a custom line spacing in dots |
| ESC d n | Feed lines | Adds a "feed" element with lines: n |
| ESC J n | Feed dots | Adds a "feed" element with dots: n |
| ESC p m t1 t2 | Cash-drawer pulse | Adds an "action" element; nothing is rendered |
| ESC i | Cut command | Adds a "cut" element with raw mode 105 |
| ESC m | Cut command | Adds a "cut" element with raw mode 109 |
| ESC * | Legacy bit image | Converts column-oriented bit data into a raster "image" element |
| ESC = n | Peripheral-device selection | Adds an "ignored-by-design" diagnostic |
| ESC U n | Hardware print mode | Adds an "ignored-by-design" diagnostic |
GS commands
| Command | Description | Parsed or rendered behavior |
| --- | --- | --- |
| GS ! n | Character size | Sets width and height multipliers from 1 through 8 |
| GS B n | Reverse print | Uses bit 0 to render white text on black |
| GS L nL nH | Left margin | Stores the left margin in dots |
| GS W nL nH | Print-area width | Stores the print-area width in dots |
| GS V m [n] | Cut | Adds a "cut" element; an extra feed byte is read for modes 65, 66, 97, and 98 |
| GS H n | HRI position | Values 1 and 3 render HRI above; 2 and 3 render it below; other values render no HRI |
| GS f n | HRI font | Stores n in the barcode element; the current renderer draws HRI with the configured Font B metrics |
| GS h n | Barcode height | Stores n; a zero value becomes 80 |
| GS w n | Barcode module width | Stores at least 1; a zero value becomes 2 |
| GS k | One-dimensional barcode | Creates a "barcode" element and validates it during rendering |
| GS ( k | QR code | Supports QR functions 65, 67, 69, 80, and 81 for cn = 49 |
| GS ( L | Raster graphics | Supports the implemented store and print graphics functions |
| GS 8 L | Large raster graphics | Uses the same implemented graphics payload handling as GS ( L |
| GS v 0 | Raster bit image | Creates a raster "image" element |
| GS * | Define downloaded image | Stores one downloaded bit image in parser state |
| GS / | Print downloaded image | Creates an "image" element from the stored downloaded image |
FS commands
| Command | Description | Parsed or rendered behavior |
| --- | --- | --- |
| FS q | Define NV images | Clears the current NV registry and reads the supplied images |
| FS p | Print NV image | Creates an "image" element or an unsupported diagnostic when the requested image is missing |
Supported content
Text
- Left, center, and right alignment
- Font A and Font B
- Bold, underline, and reverse print
- Double-width and double-height print modes
- Width and height multipliers from 1 through 8 through
GS ! - Character spacing
- Absolute and relative positioning
- Default or configured tab stops
- 90-degree rotation and upside-down text
- Left margin, print-area width, and line spacing
- Overflow policies: wrapping, clipping, or error
Images
| Parsed source value | Origin |
| --- | --- |
| "ESC *" | Legacy bit image |
| "GS v 0" | Raster bit image |
| "GS ( L" | Graphics buffered and printed through GS ( L or GS 8 L |
| "GS /" | Downloaded image printed through GS / |
| "FS p" | NV image printed through FS p |
Raster images that are wider than the current print area are proportionally reduced. Feed elements and raster images may be split across pages. Oversized text rows, barcodes, or QR codes that cannot fit within one configured page cause an EscopiLimitError.
QR codes
Escopi parses QR data through GS ( k.
| Field | Current behavior |
| --- | --- |
| Model | Model 2 is rendered |
| Model 1 | Unsupported diagnostic, then Model 2 when the unsupported-command policy permits continuation |
| Module size | Clamped to 1 through 16 while parsing |
| Error correction 48 | L |
| Error correction 49 | M |
| Error correction 50 | Q |
| Error correction 51 | H |
| Other error-correction values | The QR encoder falls back to M |
| Data | Preserved as raw bytes |
| Maximum payload | Controlled by maxQrBytes |
Barcodes
| ESC/POS mode | Symbology | Parsed data format |
| ---: | --- | --- |
| 0, 65 | UPC-A | NUL-terminated for mode 0; length-prefixed for mode 65 |
| 1, 66 | UPC-E | NUL-terminated for mode 1; length-prefixed for mode 66 |
| 2, 67 | EAN-13 | NUL-terminated for mode 2; length-prefixed for mode 67 |
| 3, 68 | EAN-8 | NUL-terminated for mode 3; length-prefixed for mode 68 |
| 4, 69 | CODE39 | NUL-terminated for mode 4; length-prefixed for mode 69 |
| 5, 70 | ITF | NUL-terminated for mode 5; length-prefixed for mode 70 |
| 6, 71 | CODABAR | NUL-terminated for mode 6; length-prefixed for mode 71 |
| 72 | CODE93 | Length-prefixed |
| 73 | CODE128 | Length-prefixed |
Validation performed before rendering:
| Symbology | Validation |
| --- | --- |
| UPC-A | 11 digits without a check digit or 12 with a valid check digit |
| UPC-E | 6 through 8 digits |
| EAN-13 | 12 digits without a check digit or 13 with a valid check digit |
| EAN-8 | 7 digits without a check digit or 8 with a valid check digit |
| ITF | Even number of digits |
| CODE39 | 0-9, A-Z, space, ., -, $, /, +, % |
| CODABAR | Starts and ends with A, B, C, or D |
| CODE93 | Validated by the bundled barcode encoder |
| CODE128 | Supports ESC/POS {A, {B, {C, and {{ selectors; set C requires digit pairs |
An unsupported GS k mode can still appear in the output of Escopi.parse() as symbology: "GS k <mode>", but rendering it throws EscopiRenderError.
Code pages
The codepage API option accepts the following exact names:
| Group | Values |
| --- | --- |
| DOS and OEM | "cp437", "cp737", "cp775", "cp850", "cp852", "cp855", "cp857", "cp858", "cp860", "cp861", "cp862", "cp863", "cp865", "cp866", "cp869" |
| Windows | "windows-1250" through "windows-1258" |
| ISO | "iso-8859-2", "iso-8859-7", "iso-8859-15" |
| Unicode | "utf-8" |
The default initial code page is "cp850".
ESC t supports the following implemented mappings:
| n | Code page | n | Code page |
| ---: | --- | ---: | --- |
| 0 | "cp437" | 33 | "cp775" |
| 2 | "cp850" | 34 | "cp855" |
| 3 | "cp860" | 35 | "cp861" |
| 4 | "cp863" | 36 | "cp862" |
| 5 | "cp865" | 38 | "cp869" |
| 13 | "windows-1252" | 39 | "iso-8859-2" |
| 14 | "cp737" | 40 | "iso-8859-15" |
| 15 | "iso-8859-7" | 45 | "windows-1250" |
| 16 | "windows-1252" | 46 | "windows-1251" |
| 17 | "cp866" | 47 | "windows-1253" |
| 18 | "cp852" | 48 | "windows-1254" |
| 19 | "cp858" | 49 | "windows-1255" |
| | | 50 | "windows-1256" |
| | | 51 | "windows-1257" |
| | | 52 | "windows-1258" |
Unknown ESC t values follow the configured unsupported policy.
ESC R accepts international-set values 0 through 13. The parser stores the numeric value and applies the implemented substitutions to the affected ASCII positions.
Diagnostics
Diagnostic structure
{
severity: "info" | "warning" | "error";
status:
| "supported"
| "ignored-by-design"
| "unsupported"
| "malformed";
message: string;
offset: number;
command: string;
}The declared status union includes "supported", but the current parser does not generate diagnostics for commands that complete successfully.
The current parser emits these combinations:
| Severity | Status | Meaning |
| --- | --- | --- |
| "info" | "ignored-by-design" | Recognized command intentionally has no PDF behavior |
| "warning" | "unsupported" | Command or value is not implemented |
| "error" | "malformed" | Command data is incomplete or invalid |
Diagnostic policies
| Option | Value | Behavior |
| --- | --- | --- |
| unsupported | "diagnostic" | Adds the diagnostic and continues |
| unsupported | "ignore" | Omits unsupported diagnostics and continues |
| unsupported | "error" | Throws EscopiUnsupportedCommandError |
| malformed | "diagnostic" | Adds the diagnostic and follows the parser's command-specific recovery path |
| malformed | "error" | Throws EscopiParseError |
"ignored-by-design" diagnostics are retained even when unsupported is "ignore".
const result = await Escopi.toPdf(input, {
unsupported: "diagnostic",
malformed: "error"
});
console.table(result.meta.diagnostics);Example diagnostic:
{
severity: "warning",
status: "unsupported",
message: "Unsupported ESC command: 120.",
offset: 48,
command: "ESC 120"
}API
| Function or property | Return type | Description |
| --- | --- | --- |
| Escopi.toPdf(input, options?) | Promise<EscopiResult> | Parses, renders, and generates a PDF |
| Escopi.toBase64(value) | Promise<string> | Converts a supported PDF value to Base64 |
| Escopi.download(result, filename?) | void | Starts a browser download |
| Escopi.revoke(result) | void | Releases and clears a result Blob URL |
| Escopi.parse(input, options?) | EscopiElements | Parses ESC/POS into intermediate elements |
| Escopi.renderToPages(elements, options?) | Promise<EscopiRenderedResult> | Loads the requested font and renders intermediate elements into canvas pages |
| Escopi.renderToCanvas(elements, options?) | Promise<EscopiCanvasResult> | Loads the requested font, renders paginated output, and combines every page into one continuous canvas |
| Escopi.PROFILES | Read-only object | Exposes the two built-in profile definitions |
| Escopi.FONT_OPTIONS | Read-only object | Exposes the supported ticket-font option values |
Examples
Escopi.toPdf()
const result = await Escopi.toPdf(input, {
profile: "80mm"
});The runtime shape is documented under Result object.
Escopi.toBase64()
Accepted values:
EscopiResultBlobUint8ArrayArrayBuffer
const result = await Escopi.toPdf(input, {
createObjectUrl: false
});
const fromResult = await Escopi.toBase64(result);
const fromBlob = await Escopi.toBase64(result.blob);
const fromBytes = await Escopi.toBase64(result.bytes);
const fromArrayBuffer = await Escopi.toBase64(result.bytes.buffer);
console.log(fromResult);Example value:
JVBERi0xLjQKJeLjz9MKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxv...The returned string contains Base64 only. It does not include a data:application/pdf;base64, prefix.
Escopi.download()
const result = await Escopi.toPdf(input);
const returnValue = Escopi.download(result, "ticket.pdf");
console.log(returnValue);undefinedThe default filename is "ticket.pdf". When result.url is null, download() creates a temporary Blob URL and revokes it after triggering the download.
Escopi.revoke()
const result = await Escopi.toPdf(input);
const returnValue = Escopi.revoke(result);
console.log(returnValue);
console.log(result.url);undefined
nullWhen the result is missing, already revoked, or has no URL, the function performs no action.
Escopi.parse()
const elements = Escopi.parse(input, {
unsupported: "diagnostic"
});
console.log(elements);
console.log(elements.diagnostics);The returned value is an array with a non-enumerable, read-only diagnostics property. Because it is non-enumerable, JSON.stringify(elements) does not include it.
Top-level element types:
| type | Description |
| --- | --- |
| "line" | Text line containing operations |
| "image" | Raster image |
| "barcode" | One-dimensional barcode |
| "qr" | QR code |
| "feed" | Vertical feed |
| "cut" | Cut marker or page-break source |
| "action" | Hardware-only action |
Line operation types:
| operation.type | Description |
| --- | --- |
| "text" | Decoded text and the active style |
| "tab" | Horizontal tab |
| "position" | Absolute or relative dot positioning |
Example:
[
{
type: "line",
align: "center",
lineSpacingDots: null,
leftMarginDots: 0,
printAreaWidthDots: null,
tabStops: [],
operations: [
{
type: "text",
text: "STORE NAME",
style: {
bold: true,
underline: 0,
invert: false,
font: "A",
widthMul: 1,
heightMul: 1,
codepage: "cp850",
internationalSet: 0,
charSpacingDots: 0,
rotate90: false,
upsideDown: false
}
}
]
},
{
type: "feed",
lines: 1,
dots: null
},
{
type: "cut",
mode: 66,
feedDots: 2,
full: false
}
]Parsed element reference
"line"
{
type: "line";
align: "left" | "center" | "right";
lineSpacingDots: number | null;
leftMarginDots: number;
printAreaWidthDots: number | null;
tabStops: number[];
operations: Array<TextOperation | TabOperation | PositionOperation>;
}Text operation:
{
type: "text";
text: string;
style: {
bold: boolean;
underline: number;
invert: boolean;
font: "A" | "B";
widthMul: number;
heightMul: number;
codepage: string;
internationalSet: number;
charSpacingDots: number;
rotate90: boolean;
upsideDown: boolean;
};
}Current parser ranges:
| Field | Values produced by the parser |
| --- | --- |
| align | "left", "center", "right" |
| underline | 0, 1, 2 |
| font | "A", "B" |
| widthMul | 1 through 8 |
| heightMul | 1 through 8 |
| internationalSet | 0 through 13 |
| charSpacingDots | Byte value 0 through 255 |
| rotate90 | false, true |
| upsideDown | false, true |
Tab operation:
{
type: "tab";
}Position operation:
{
type: "position";
mode: "absolute" | "relative";
dots: number;
}"image"
{
type: "image";
data: Uint8Array;
widthBytes: number;
widthDots: number;
heightDots: number;
scaleX: number;
scaleY: number;
align: "left" | "center" | "right";
source: "ESC *" | "GS v 0" | "GS ( L" | "GS /" | "FS p";
leftMarginDots: number;
printAreaWidthDots: number | null;
}"barcode"
{
type: "barcode";
align: "left" | "center" | "right";
mode: number;
symbology: string;
data: Uint8Array;
text: string;
heightDots: number;
moduleWidth: number;
hriPosition: number;
hriFont: number;
leftMarginDots: number;
printAreaWidthDots: number | null;
}Supported symbology values are:
UPC-A
UPC-E
EAN-13
EAN-8
CODE39
ITF
CODABAR
CODE93
CODE128For an unknown mode, Escopi.parse() uses "GS k <mode>". Rendering that element fails because the barcode encoder does not support the mode.
Recognized HRI positions:
| hriPosition | PDF behavior |
| ---: | --- |
| 0 | No HRI |
| 1 | Above |
| 2 | Below |
| 3 | Above and below |
| Other | No HRI |
"qr"
{
type: "qr";
align: "left" | "center" | "right";
data: Uint8Array;
model: number;
moduleSize: number;
errorCorrection: number;
leftMarginDots: number;
printAreaWidthDots: number | null;
}In normal renderable output, model is 2, moduleSize is between 1 and 16, and the standard error-correction byte values are 48, 49, 50, and 51.
"feed"
Line-based feed:
{
type: "feed",
lines: 3,
dots: null
}Dot-based feed:
{
type: "feed",
lines: null,
dots: 80
}"cut"
{
type: "cut";
mode: number;
feedDots: number;
full: boolean;
}mode preserves the raw command byte. In the current implementation, full is true only for mode values 0, 65, and 97; it is false for other modes. PDF cut rendering and page breaks do not inspect full: every cut element follows showCut and cutAsPageBreak.
"action"
{
type: "action";
action: "cash-drawer";
data: {
pin: number;
onTime: number;
offTime: number;
};
}The current parser produces only the "cash-drawer" action.
Font metadata
toPdf(), renderToPages(), and renderToCanvas() report the resolved font:
{
requested: "departure-mono" | "monospace";
used: "departure-mono" | "monospace";
family: string;
loaded: boolean;
fallbackUsed: boolean;
}Default font loaded successfully:
{
requested: "departure-mono",
used: "departure-mono",
family: "\"Escopi Departure Mono\", monospace",
loaded: true,
fallbackUsed: false
}Default font unavailable and fallback used:
{
requested: "departure-mono",
used: "monospace",
family: "monospace",
loaded: false,
fallbackUsed: true
}Escopi.renderToPages()
const elements = Escopi.parse(input);
const rendered = await Escopi.renderToPages(elements, {
profile: "80mm"
});Runtime structure:
{
pages: Array<{
canvas: HTMLCanvasElement | OffscreenCanvas;
widthMm: number;
heightMm: number;
}>;
paperWidthMm: number;
printableWidthDots: number;
profile: string;
columns: {
fontA: number;
fontB: number;
};
font: EscopiFontMetadata;
}Inspect a browser result:
console.log(rendered.profile);
console.log(rendered.paperWidthMm);
console.log(rendered.printableWidthDots);
console.log(rendered.columns);
console.log(rendered.font);
console.log(rendered.pages.length);
const firstPage = rendered.pages[0];
console.log(firstPage.canvas.width);
console.log(firstPage.canvas.height);
console.log(firstPage.widthMm);
console.log(firstPage.heightMm);For the default 80 mm profile, the canvas width is 640 pixels because 80 * 8 = 640. Page height depends on content and pagination.
Append all pages:
for (const page of rendered.pages) {
document.body.appendChild(page.canvas);
}renderToPages() does not return a pageCount property. Use rendered.pages.length.
Escopi.renderToCanvas()
const elements = Escopi.parse(input);
const rendered = await Escopi.renderToCanvas(elements, {
profile: "58mm"
});It first performs the same pagination as renderToPages(). It then stacks every rendered page vertically into one continuous canvas.
It returns every property from renderToPages() plus:
{
canvas: HTMLCanvasElement | OffscreenCanvas;
heightMm: number;
}canvas contains the complete ticket, not only the first page. heightMm is the sum of all rendered page heights:
let pageHeightPx = 0;
let pageHeightMm = 0;
for (const page of rendered.pages) {
pageHeightPx += page.canvas.height;
pageHeightMm += page.heightMm;
}
console.log(rendered.canvas.width === rendered.pages[0].canvas.width);
console.log(rendered.canvas.height === pageHeightPx);
console.log(rendered.heightMm === pageHeightMm);true
true
trueThe original paginated canvases remain available in rendered.pages. This makes renderToCanvas() useful when a single continuous image is required while still preserving access to page boundaries.
Creating one combined canvas is subject to the canvas-size limits of the current browser. If the browser cannot allocate the complete canvas, Escopi throws EscopiRenderError.
Escopi.FONT_OPTIONS
console.log(Escopi.FONT_OPTIONS);{
DEFAULT: "departure-mono",
DEPARTURE_MONO: "departure-mono",
MONOSPACE: "monospace"
}The object is frozen.
Escopi.PROFILES
console.log(Escopi.PROFILES);Actual structure:
{
"58mm": {
paperWidthMm: 58,
printableWidthDots: 384,
dotsPerMm: 8,
fontAWidthDots: 12,
fontAHeightDots: 24,
fontBWidthDots: 9,
fontBHeightDots: 17
},
"80mm": {
paperWidthMm: 80,
printableWidthDots: 576,
dotsPerMm: 8,
fontAWidthDots: 12,
fontAHeightDots: 24,
fontBWidthDots: 9,
fontBHeightDots: 17
}
}The object and both profile definitions are frozen.
Errors
All Escopi error classes extend EscopiError.
| Error class | code | Typical cause |
| --- | --- | --- |
| EscopiInputError | "ESCOPI_INPUT_ERROR" | Invalid input or invalid API argument |
| EscopiLimitError | "ESCOPI_LIMIT_ERROR" | Configured resource limit exceeded |
| EscopiParseError | "ESCOPI_PARSE_ERROR" | Malformed ESC/POS data or unsupported initial code page |
| EscopiUnsupportedCommandError | "ESCOPI_UNSUPPORTED_COMMAND" | Unsupported command with unsupported: "error" |
| EscopiRenderError | "ESCOPI_RENDER_ERROR" | Canvas, layout, image, QR, or barcode rendering failure |
| EscopiPdfError | "ESCOPI_PDF_ERROR" | Blob or PDF generation is unavailable or failed |
| EscopiError | "ESCOPI_BROWSER_REQUIRED" | download() was called without a browser document |
Every error exposes:
{
name: string;
message: string;
code: string;
details: Record<string, unknown>;
}details is context-dependent. It may contain fields such as actual, limit, cause, offset, command, severity, or status.
try {
await Escopi.toPdf(input, {
unsupported: "error"
});
} catch (error) {
if (error instanceof Escopi.EscopiUnsupportedCommandError) {
console.error(error.code);
console.error(error.message);
console.error(error.details);
}
}Browser requirements
Escopi requires the following APIs for complete PDF generation:
HTMLCanvasElementthroughdocument.createElement("canvas"), orOffscreenCanvas- Canvas 2D rendering context
BlobTextEncoder- Typed arrays
Additional APIs are conditional:
| API | Required for |
| --- | --- |
| URL.createObjectURL() | Creating result.url and downloading a result that has no existing URL |
| document | DOM canvas creation and Escopi.download() |
| FontFace and a FontFaceSet (document.fonts or self.fonts) | Loading and registering the embedded Departure Mono font; absence triggers the monospace fallback |
| TextDecoder | Explicit "utf-8" decoding |
| CompressionStream | Optional PDF image compression |
| Response | Reading the optional compressed stream |
When CompressionStream is unavailable or compression fails, Escopi writes uncompressed image streams instead.
Escopi is browser-first. Importing it in Node.js does not provide a Canvas implementation. Rendering in Node requires compatible browser-equivalent Canvas and related APIs.
Development
npm install
npm run buildThe build produces:
| File | Format |
| --- | --- |
| dist/escopi.js | Browser IIFE |
| dist/escopi.min.js | Minified browser IIFE |
| dist/escopi.esm.min.js | Minified ES module |
| dist/index.d.ts | TypeScript declarations |
