Files
barcode-tools/src/barcode/generate.ts
T

156 lines
4.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { toSVG } from "bwip-js/browser";
import { sanitizeDownloadFilename } from "@add-ideas/toolbox-helpers";
import { strToU8, zipSync } from "fflate";
export const BARCODE_FORMATS = [
["qrcode", "QR Code"],
["code128", "Code 128"],
["gs1-128", "GS1-128"],
["ean13", "EAN-13"],
["ean8", "EAN-8"],
["upca", "UPC-A"],
["code39", "Code 39"],
["datamatrix", "Data Matrix"],
["pdf417", "PDF417"],
["azteccode", "Aztec Code"],
] as const;
export type BarcodeFormat = (typeof BARCODE_FORMATS)[number][0];
export interface BarcodeOptions {
format: BarcodeFormat;
text: string;
scale: number;
padding: number;
includeText: boolean;
}
export interface BarcodeSvg {
svg: string;
width: number;
height: number;
quietZone: string;
}
const LINEAR = new Set<BarcodeFormat>([
"code128",
"gs1-128",
"ean13",
"ean8",
"upca",
"code39",
]);
export function renderBarcodeSvg(options: BarcodeOptions): BarcodeSvg {
if (!BARCODE_FORMATS.some(([format]) => format === options.format))
throw new Error("Unsupported barcode format.");
if (!options.text || options.text.length > 4_096)
throw new Error("Payload must contain 14,096 characters.");
if (
!Number.isInteger(options.scale) ||
options.scale < 1 ||
options.scale > 8
)
throw new Error("Scale must be an integer from 1 to 8.");
if (
!Number.isInteger(options.padding) ||
options.padding < 0 ||
options.padding > 64
)
throw new Error("Padding must be an integer from 0 to 64.");
const linear = LINEAR.has(options.format);
const svg = toSVG({
bcid: options.format,
text: options.text,
scale: options.scale,
...(linear ? { height: 15, includetext: options.includeText } : {}),
padding: options.padding,
backgroundcolor: "FFFFFF",
barcolor: "111111",
textcolor: "111111",
});
if (/<(?:script|foreignObject)|\bon\w+\s*=|\b(?:href|src)\s*=/iu.test(svg))
throw new Error("Generated SVG failed the inert-output policy.");
const viewBox = /viewBox="[^"]*?([\d.]+)\s+([\d.]+)"/u.exec(svg);
const width = Number(viewBox?.[1] ?? /width="([\d.]+)"/u.exec(svg)?.[1] ?? 0);
const height = Number(
viewBox?.[2] ?? /height="([\d.]+)"/u.exec(svg)?.[1] ?? 0,
);
return {
svg,
width,
height,
quietZone:
options.format === "qrcode"
? "At least 4 modules on every side"
: options.format === "datamatrix"
? "At least 1 module on every side"
: "At least 10 narrow modules at left and right",
};
}
export interface BatchRow {
name: string;
value: string;
}
function parseCsvLine(line: string): string[] {
const cells: string[] = [];
let cell = "";
let quoted = false;
for (let index = 0; index < line.length; index += 1) {
const character = line[index]!;
if (quoted && character === '"' && line[index + 1] === '"') {
cell += '"';
index += 1;
} else if (character === '"') quoted = !quoted;
else if (character === "," && !quoted) {
cells.push(cell);
cell = "";
} else cell += character;
}
if (quoted) throw new Error("A batch row contains an unclosed quote.");
cells.push(cell);
return cells;
}
export function parseBatch(source: string): BatchRow[] {
if (source.length > 256 * 1024)
throw new Error("Batch input exceeds 256 KiB.");
const rows = source
.replaceAll("\r\n", "\n")
.split("\n")
.filter((line) => line.trim())
.map((line, index) => {
const [name, value, ...extra] = parseCsvLine(line);
if (extra.length || !name?.trim() || value === undefined || !value.trim())
throw new Error(`Batch row ${index + 1} must contain name,value.`);
return { name: name.trim(), value };
});
if (rows.length > 100) throw new Error("A batch is limited to 100 barcodes.");
return rows;
}
export function createBarcodeZip(
rows: BatchRow[],
options: Omit<BarcodeOptions, "text">,
): Uint8Array {
const files: Record<string, Uint8Array> = {};
const used = new Set<string>();
for (const [index, row] of rows.entries()) {
let name = sanitizeDownloadFilename(
row.name,
`barcode-${index + 1}`,
).replace(/\.svg$/iu, "");
if (!name) name = `barcode-${index + 1}`;
let unique = name;
for (let counter = 2; used.has(unique.toLowerCase()); counter += 1)
unique = `${name}-${counter}`;
used.add(unique.toLowerCase());
files[`${unique}.svg`] = strToU8(
renderBarcodeSvg({ ...options, text: row.value }).svg,
);
}
return zipSync(files, { level: 6, mtime: new Date("1980-01-01T00:00:00Z") });
}