Initial release of Barcode Tools 0.1.0

This commit is contained in:
2026-09-01 02:55:06 +02:00
commit 81677e1134
58 changed files with 9200 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
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") });
}