Files

795 lines
35 KiB
HTML
Raw Permalink Normal View History

2026-09-13 20:05:01 +01:00
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Barcode Studio</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fraunces:opsz,wght@9..144,300;9..144,400;9..144,500;9..144,700;9..144,900&family=JetBrains+Mono:wght@300;400;500;700&display=swap" rel="stylesheet">
<!-- Barcode libraries (vendored locally — pinned versions, no CDN dependency) -->
<script
src="vendor/JsBarcode.all.min.js"
integrity="sha384-vmcSy8TM1KhZWBIKMKTR8AxbrJQCuConAolGY+42odu9ZGIzw8L8xAT/u7ul4X2U"
crossorigin="anonymous"></script>
<script
src="vendor/bwip-js-min.js"
integrity="sha384-33pyXSGFs/ylT0uDwaLiFGZbH9AnIjaVhTWbD3HHFxyS0iGlnUpC5L7Vs/1gA1vw"
crossorigin="anonymous"></script>
<style>
:root{
--bg: #f1ece4;
--bg-2: #e8e2d6;
--ink: #14110d;
--ink-soft: #3a342b;
--accent: #ff4a1c;
--accent-2: #1b3aff;
--paper: #fbf8f2;
--line: #14110d;
--rule: rgba(20,17,13,.18);
--grain-opacity: .06;
}
*{box-sizing:border-box;margin:0;padding:0}
html,body{background:var(--bg);color:var(--ink);font-family:'JetBrains Mono',monospace;font-size:14px;min-height:100vh;overflow-x:hidden}
/* Paper grain */
body::before{
content:'';position:fixed;inset:0;pointer-events:none;z-index:1;
background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='200' height='200'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.6 0'/></filter><rect width='100%25' height='100%25' filter='url(%23n)'/></svg>");
opacity:var(--grain-opacity);mix-blend-mode:multiply;
}
/* ===== Header ===== */
header{
position:relative;z-index:2;
border-bottom:1px solid var(--line);
padding:14px 28px;
display:flex;align-items:center;justify-content:space-between;
background:var(--bg);
}
.brand{display:flex;align-items:baseline;gap:14px}
.brand-mark{
font-family:'Fraunces',serif;font-weight:900;font-size:28px;letter-spacing:-.02em;
font-style:italic;
}
.brand-mark::after{
content:'';display:inline-block;width:10px;height:10px;background:var(--accent);margin-left:6px;transform:translateY(-2px);
}
.brand-sub{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-soft)}
.meta{display:flex;gap:24px;font-size:11px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-soft)}
.meta span b{color:var(--ink);font-weight:500}
.blink{display:inline-block;width:8px;height:8px;background:var(--accent);border-radius:50%;margin-right:6px;animation:pulse 1.6s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}
/* ===== Hero strip ===== */
.hero{
position:relative;z-index:2;
border-bottom:1px solid var(--line);
padding:42px 28px 36px;
display:grid;grid-template-columns:1.4fr 1fr;gap:40px;align-items:end;
background:linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
}
.hero h1{
font-family:'Fraunces',serif;font-weight:300;
font-size:clamp(48px, 7vw, 96px);
line-height:.92;letter-spacing:-.035em;
}
.hero h1 em{font-style:italic;font-weight:500;color:var(--accent)}
.hero h1 .strike{text-decoration:line-through;text-decoration-thickness:2px;font-weight:900}
.hero-desc{
font-size:13px;line-height:1.6;color:var(--ink-soft);max-width:420px;
border-left:2px solid var(--accent);padding-left:14px;
}
.hero-desc b{color:var(--ink);font-weight:500;text-transform:uppercase;letter-spacing:.1em;font-size:11px;display:block;margin-bottom:8px}
/* ===== Main layout ===== */
main{
position:relative;z-index:2;
display:grid;grid-template-columns:360px 1fr;min-height:calc(100vh - 220px);
}
/* ===== Sidebar: format picker ===== */
.sidebar{
border-right:1px solid var(--line);
background:var(--bg);
overflow-y:auto;
max-height:calc(100vh - 110px);
position:sticky;top:0;
}
.sidebar-head{
padding:18px 22px 14px;
border-bottom:1px solid var(--rule);
display:flex;justify-content:space-between;align-items:baseline;
position:sticky;top:0;background:var(--bg);z-index:2;
}
.sidebar-head h2{
font-family:'Fraunces',serif;font-weight:500;font-style:italic;font-size:18px;
}
.sidebar-head .count{font-size:10px;letter-spacing:.18em;color:var(--ink-soft);text-transform:uppercase}
.group{padding:10px 0;border-bottom:1px solid var(--rule)}
.group-title{
padding:14px 22px 6px;font-size:10px;letter-spacing:.22em;text-transform:uppercase;
color:var(--ink-soft);display:flex;align-items:center;gap:8px;
}
.group-title::before{content:'';flex:0 0 14px;height:1px;background:var(--ink-soft)}
.fmt{
padding:11px 22px;cursor:pointer;display:flex;align-items:center;justify-content:space-between;gap:10px;
transition:background .15s, padding .15s;border-left:3px solid transparent;
}
.fmt:hover{background:var(--bg-2);padding-left:26px}
.fmt.active{background:var(--ink);color:var(--paper);border-left-color:var(--accent)}
.fmt.active .fmt-code{color:var(--paper);opacity:.6}
.fmt-name{font-size:13px;font-weight:500;letter-spacing:-.005em}
.fmt-code{font-size:10px;letter-spacing:.1em;color:var(--ink-soft);text-transform:uppercase}
/* ===== Workspace ===== */
.workspace{padding:28px 32px 80px;background:var(--bg-2);}
.selected-card{
background:var(--paper);
border:1px solid var(--line);
padding:22px 24px;
margin-bottom:24px;
display:grid;grid-template-columns:1fr auto;gap:24px;align-items:center;
position:relative;
}
.selected-card::before{
content:'01';position:absolute;top:-10px;left:18px;background:var(--accent);color:var(--paper);
font-size:10px;letter-spacing:.2em;padding:2px 8px;font-weight:700;
}
.selected-meta h3{
font-family:'Fraunces',serif;font-weight:500;font-size:32px;letter-spacing:-.02em;margin-bottom:4px;
}
.selected-meta .selected-type{font-size:11px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-soft);margin-bottom:14px}
.selected-meta p{font-size:12px;line-height:1.65;color:var(--ink-soft);max-width:520px}
.selected-spec{
font-size:10px;letter-spacing:.14em;text-transform:uppercase;color:var(--ink-soft);
display:flex;gap:18px;margin-top:14px;flex-wrap:wrap;
}
.selected-spec b{color:var(--ink);font-weight:500}
.example-box{
width:260px;background:#fff;border:1px solid var(--rule);padding:14px;
display:flex;flex-direction:column;align-items:center;gap:8px;position:relative;
}
.example-box::before{
content:'EXAMPLE';position:absolute;top:-8px;left:10px;background:var(--paper);
font-size:9px;letter-spacing:.22em;color:var(--ink-soft);padding:0 6px;
}
.example-box canvas, .example-box svg{max-width:100%;height:auto;display:block}
.example-box .empty{font-size:10px;color:var(--ink-soft);letter-spacing:.1em;text-transform:uppercase;padding:20px 0}
/* ===== Input area ===== */
.input-section{
background:var(--paper);
border:1px solid var(--line);
padding:22px 24px 24px;
margin-bottom:24px;
position:relative;
}
.input-section::before{
content:'02';position:absolute;top:-10px;left:18px;background:var(--accent);color:var(--paper);
font-size:10px;letter-spacing:.2em;padding:2px 8px;font-weight:700;
}
.input-head{
display:flex;justify-content:space-between;align-items:baseline;margin-bottom:14px;flex-wrap:wrap;gap:12px;
}
.input-head h3{font-family:'Fraunces',serif;font-weight:500;font-style:italic;font-size:22px;letter-spacing:-.01em}
.input-help{font-size:11px;color:var(--ink-soft);letter-spacing:.06em}
.input-help kbd{background:var(--bg-2);border:1px solid var(--rule);padding:1px 5px;font-family:inherit;font-size:10px;border-radius:2px}
textarea{
width:100%;min-height:140px;resize:vertical;
background:#fff;border:1px solid var(--rule);
padding:14px 16px;
font-family:'JetBrains Mono',monospace;font-size:13px;line-height:1.7;
color:var(--ink);outline:none;transition:border-color .15s, box-shadow .15s;
}
textarea:focus{border-color:var(--ink);box-shadow:0 0 0 3px rgba(255,74,28,.15)}
.options-row{
display:flex;gap:24px;margin-top:16px;flex-wrap:wrap;align-items:center;
}
.opt{display:flex;align-items:center;gap:8px;font-size:11px;letter-spacing:.08em;text-transform:uppercase;color:var(--ink-soft);cursor:pointer}
.opt input[type="checkbox"]{
appearance:none;width:14px;height:14px;border:1.5px solid var(--ink);background:transparent;cursor:pointer;position:relative;
}
.opt input[type="checkbox"]:checked{background:var(--ink)}
.opt input[type="checkbox"]:checked::after{
content:'';position:absolute;inset:2px;background:var(--accent);
}
.opt-num{display:flex;align-items:center;gap:6px}
.opt-num input{
width:60px;background:#fff;border:1px solid var(--rule);padding:4px 8px;
font-family:inherit;font-size:11px;color:var(--ink);outline:none;
}
.opt-num input:focus{border-color:var(--ink)}
.actions{display:flex;gap:12px;margin-top:18px;flex-wrap:wrap}
button.btn{
font-family:'JetBrains Mono',monospace;
font-size:11px;letter-spacing:.18em;text-transform:uppercase;font-weight:700;
padding:14px 22px;cursor:pointer;border:1px solid var(--ink);
transition:all .15s;display:inline-flex;align-items:center;gap:10px;
}
.btn-primary{background:var(--ink);color:var(--paper)}
.btn-primary:hover{background:var(--accent);border-color:var(--accent);transform:translate(-2px,-2px);box-shadow:4px 4px 0 var(--ink)}
.btn-secondary{background:transparent;color:var(--ink)}
.btn-secondary:hover{background:var(--ink);color:var(--paper)}
.btn-danger:hover{background:var(--accent);color:var(--paper);border-color:var(--accent)}
/* ===== Results ===== */
.results-section{
background:var(--paper);
border:1px solid var(--line);
padding:22px 24px 28px;
position:relative;min-height:200px;
}
.results-section::before{
content:'03';position:absolute;top:-10px;left:18px;background:var(--accent);color:var(--paper);
font-size:10px;letter-spacing:.2em;padding:2px 8px;font-weight:700;
}
.results-head{
display:flex;justify-content:space-between;align-items:baseline;margin-bottom:18px;flex-wrap:wrap;gap:12px;
padding-bottom:12px;border-bottom:1px solid var(--rule);
}
.results-head h3{font-family:'Fraunces',serif;font-weight:500;font-style:italic;font-size:22px;letter-spacing:-.01em}
.results-stats{font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-soft)}
.results-stats b{color:var(--accent);font-weight:700}
.results-empty{
padding:60px 20px;text-align:center;color:var(--ink-soft);
font-size:11px;letter-spacing:.18em;text-transform:uppercase;
}
.results-empty::before{
content:'⌗';display:block;font-size:54px;margin-bottom:14px;color:var(--rule);font-family:'Fraunces',serif;
}
.results-grid{
display:grid;grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));gap:18px;
}
.result-card{
background:#fff;border:1px solid var(--rule);padding:18px;
display:flex;flex-direction:column;gap:12px;
transition:transform .15s, box-shadow .15s;
position:relative;
}
.result-card:hover{transform:translate(-2px,-2px);box-shadow:4px 4px 0 var(--ink)}
.result-card .idx{
position:absolute;top:8px;right:10px;font-size:9px;letter-spacing:.14em;
color:var(--ink-soft);
}
.result-canvas-wrap{
background:#fff;display:flex;align-items:center;justify-content:center;
min-height:120px;padding:6px;border:1px dashed var(--rule);
}
.result-canvas-wrap canvas, .result-canvas-wrap svg{max-width:100%;height:auto;display:block}
.result-data{
font-size:11px;color:var(--ink-soft);word-break:break-all;
border-top:1px solid var(--rule);padding-top:10px;
max-height:50px;overflow:hidden;text-overflow:ellipsis;
}
.result-data b{color:var(--ink);font-weight:500}
.result-actions{display:flex;gap:6px}
.result-actions button{
flex:1;font-family:inherit;font-size:9px;letter-spacing:.14em;text-transform:uppercase;
padding:7px 8px;background:transparent;border:1px solid var(--ink);color:var(--ink);cursor:pointer;
transition:all .12s;font-weight:700;
}
.result-actions button:hover{background:var(--ink);color:var(--paper)}
.result-error{
background:#ffe9e2;border:1px solid var(--accent);color:var(--accent);
padding:14px;font-size:11px;line-height:1.55;
}
/* Toast */
.toast{
position:fixed;bottom:24px;left:50%;transform:translateX(-50%) translateY(80px);
background:var(--ink);color:var(--paper);padding:12px 22px;font-size:11px;letter-spacing:.18em;text-transform:uppercase;
z-index:50;transition:transform .3s cubic-bezier(.2,.8,.2,1);
border:1px solid var(--accent);
}
.toast.show{transform:translateX(-50%) translateY(0)}
/* Footer */
footer{
border-top:1px solid var(--line);padding:18px 28px;display:flex;justify-content:space-between;align-items:center;
font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--ink-soft);background:var(--bg);position:relative;z-index:2;
}
footer .dots{display:flex;gap:4px}
footer .dots span{width:6px;height:6px;background:var(--ink);border-radius:50%}
footer .dots span:nth-child(2){background:var(--accent)}
footer .dots span:nth-child(3){background:var(--accent-2)}
/* Responsive */
@media (max-width: 1000px){
main{grid-template-columns:1fr}
.sidebar{position:static;max-height:none;border-right:none;border-bottom:1px solid var(--line)}
.hero{grid-template-columns:1fr;gap:20px}
.selected-card{grid-template-columns:1fr}
.example-box{width:100%}
}
@media (max-width: 600px){
header{flex-direction:column;gap:10px;align-items:flex-start}
.meta{flex-wrap:wrap;gap:10px}
.hero{padding:28px 18px}
.workspace{padding:18px}
}
/* Scrollbar */
.sidebar::-webkit-scrollbar{width:8px}
.sidebar::-webkit-scrollbar-track{background:transparent}
.sidebar::-webkit-scrollbar-thumb{background:var(--ink);border-radius:0}
</style>
</head>
<body>
<header>
<div class="brand">
<div class="brand-mark">Barcode Studio · v1.1</div>
<div class="brand-sub">J.Vaz 2026</div>
</div>
<div class="meta">
<span><span class="blink"></span>System Live</span>
<span>Formats <b>22</b></span>
<span>Output <b>JPG / PNG</b></span>
</div>
</header>
<section class="hero">
<h1>Encode <em>anything</em>.<br><span class="strike">Print</span> everything.</h1>
<div class="hero-desc">
<b>Studio Manual</b>
A precision instrument for converting text and numbers into industrial-grade barcodes.
Select a symbology, paste your data line-by-line, and export downloadable JPG images —
one per line, or all at once.
</div>
</section>
<main>
<!-- Sidebar: format picker -->
<aside class="sidebar">
<div class="sidebar-head">
<h2>Symbologies</h2>
<span class="count" id="formatCount">— FORMATS</span>
</div>
<div id="formatList"></div>
</aside>
<!-- Workspace -->
<div class="workspace">
<!-- Selected format card -->
<div class="selected-card">
<div class="selected-meta">
<h3 id="selName"></h3>
<div class="selected-type" id="selType">Select a format</div>
<p id="selDesc">Pick a symbology from the panel on the left to begin.</p>
<div class="selected-spec" id="selSpec"></div>
</div>
<div class="example-box">
<div id="exampleWrap"><div class="empty">Loading…</div></div>
</div>
</div>
<!-- Input area -->
<div class="input-section">
<div class="input-head">
<h3>Data Input</h3>
<div class="input-help">One barcode per line · <kbd></kbd> to add another</div>
</div>
<textarea id="dataInput" placeholder="Enter your data here…
Each line becomes a separate barcode.
123456789012
HELLO-WORLD
https://example.com"></textarea>
<div class="options-row">
<label class="opt">
<input type="checkbox" id="optShowText" checked>
<span>Show text below</span>
</label>
<label class="opt opt-num">
<span>Scale</span>
<input type="number" id="optScale" value="3" min="1" max="10" step="1">
</label>
<label class="opt opt-num">
<span>Quality</span>
<input type="number" id="optQuality" value="0.95" min="0.5" max="1" step="0.05">
</label>
</div>
<div class="actions">
<button class="btn btn-primary" id="btnGenerate">
<span></span> Generate Barcodes
</button>
<button class="btn btn-secondary" id="btnDownloadAll">
<span></span> Download All as JPG
</button>
<button class="btn btn-secondary btn-danger" id="btnClear">
<span>×</span> Clear
</button>
</div>
</div>
<!-- Results -->
<div class="results-section">
<div class="results-head">
<h3>Output</h3>
<div class="results-stats" id="resultsStats">Generated · <b>0</b></div>
</div>
<div id="results">
<div class="results-empty">No barcodes generated yet</div>
</div>
</div>
</div>
</main>
<footer>
<div>Barcode Studio</div>
<div class="dots"><span></span><span></span><span></span></div>
<div>JPG · PNG · Print-Ready</div>
</footer>
<div class="toast" id="toast">Saved</div>
<script>
/* =========================================================
Barcode format definitions
========================================================= */
const FORMATS = [
// ---------- Retail / EAN-UPC family ----------
{ id:'ean13', name:'EAN-13', group:'Retail · GS1', engine:'jsbarcode', code:'ean13', example:'5901234123457', desc:'European Article Number — the standard 13-digit retail barcode used on consumer products worldwide.', spec:['Numeric','13 digits','Check digit auto'] },
{ id:'ean8', name:'EAN-8', group:'Retail · GS1', engine:'jsbarcode', code:'ean8', example:'96385074', desc:'Compact 8-digit version of EAN-13, used on small packaging where space is limited.', spec:['Numeric','8 digits','Compact'] },
{ id:'upca', name:'UPC-A', group:'Retail · GS1', engine:'jsbarcode', code:'upc', example:'036000291452', desc:'Universal Product Code, the dominant retail barcode in North America. 12 digits.', spec:['Numeric','12 digits','USA standard'] },
{ id:'upce', name:'UPC-E', group:'Retail · GS1', engine:'jsbarcode', code:'upce', example:'01234565', desc:'Compressed 8-digit UPC variant for small retail items.', spec:['Numeric','8 digits','Compressed'] },
{ id:'isbn', name:'ISBN-13', group:'Retail · GS1', engine:'jsbarcode', code:'ean13', example:'9783161484100',desc:'International Standard Book Number — 13-digit barcode for books and periodicals.', spec:['Numeric','13 digits','Books'] },
{ id:'gs1databar', name:'GS1 DataBar', group:'Retail · GS1', engine:'bwip', code:'databaromni', example:'(01)24012345678905', desc:'Compact GS1 symbology designed for fresh foods, coupons, and small healthcare items.', spec:['Numeric · 14 digits','GS1 Application Identifier','Omnidirectional'] },
{ id:'gs1-128',name:'GS1-128', group:'Retail · GS1', engine:'bwip', code:'gs1-128', example:'(01)12345678901231(17)260101', desc:'GS1 application of Code 128 — used in supply chain, logistics, and shipping. Encodes Application Identifiers.', spec:['Alphanumeric','Variable length','Supply chain'] },
// ---------- Linear / Industrial ----------
{ id:'code128',name:'Code-128', group:'Linear · Industrial', engine:'jsbarcode', code:'CODE128', example:'CIPHER-2026-A1', desc:'High-density linear barcode. Encodes all 128 ASCII characters and is widely used in shipping and packaging.', spec:['ASCII','Variable','High density'] },
{ id:'code39', name:'Code-39', group:'Linear · Industrial', engine:'jsbarcode', code:'CODE39', example:'CODE 39 EXAMPLE', desc:'Discrete, self-checking symbology. Encodes 43 characters: AZ, 09, and a few symbols. Common in defense and industry.', spec:['AZ 09','Self-checking','Industrial'] },
{ id:'code39e',name:'Code-39 Full ASCII', group:'Linear · Industrial', engine:'bwip', code:'code39ext', example:'Code 39 ext.', desc:'Extended Code-39 that uses paired characters to encode the full 128-character ASCII set.', spec:['Full ASCII','128 chars','Paired encoding'] },
{ id:'code93', name:'Code-93', group:'Linear · Industrial', engine:'jsbarcode', code:'CODE93', example:'CODE93EXAMPLE', desc:'Denser successor to Code-39 with two built-in check digits for improved reliability.', spec:['ASCII','2 check digits','Compact'] },
{ id:'code11', name:'Code-11', group:'Linear · Industrial', engine:'bwip', code:'code11', example:'0123456789-', desc:'Numeric symbology developed for telecom equipment labeling. High density for digits and dashes.', spec:['09 + dash','Telecom','Numeric'] },
{ id:'i25', name:'Code 2of5 Interleaved', group:'Linear · Industrial', engine:'jsbarcode', code:'ITF', example:'1234567890', desc:'Interleaved 2 of 5 — efficient numeric-only symbology used in warehousing and ITF-14 cartons.', spec:['Numeric','Even length','Warehouse'] },
{ id:'msi', name:'MSI Plessey', group:'Linear · Industrial', engine:'jsbarcode', code:'MSI', example:'1234567', desc:'Modified Plessey code, used historically for inventory and warehouse shelf marking.', spec:['Numeric','Variable','Inventory'] },
{ id:'flatter',name:'Flattermarken', group:'Linear · Industrial', engine:'bwip', code:'flattermarken', example:'12345678', desc:'Specialty code used in the print industry to track signatures and folded sections of books.', spec:['Numeric','Print industry','Signatures'] },
{ id:'telepen',name:'Telepen Alpha', group:'Linear · Industrial', engine:'bwip', code:'telepen', example:'Telepen', desc:'Full-ASCII linear barcode developed in the UK, common in libraries and academic settings.', spec:['Full ASCII','UK origin','Libraries'] },
// ---------- Pharma ----------
{ id:'pharma1',name:'Pharmacode One-Track', group:'Pharma · Specialty', engine:'bwip', code:'pharmacode', example:'1234', desc:'Pharmaceutical binary code (Laetus). Verifies pharmaceutical packaging during production.', spec:['Numeric 3131070','Binary bars','Pharma QA'] },
{ id:'pharma2',name:'Pharmacode Two-Track', group:'Pharma · Specialty', engine:'bwip', code:'pharmacode2', example:'12345', desc:'Two-track variant of Pharmacode — encodes data using bar height in addition to position.', spec:['Numeric 464570','Two heights','Pharma QA'] },
// ---------- Postal ----------
{ id:'kix', name:'Postal · KIX', group:'Postal', engine:'bwip', code:'kix', example:'1234AB56P7', desc:'Dutch PostNL routing code. Encodes postal codes and house numbers for Dutch mail.', spec:['Alphanumeric','Netherlands','Routing'] },
// ---------- 2D ----------
{ id:'qr', name:'QR Code', group:'2D · Matrix', engine:'bwip', code:'qrcode', example:'https://anthropic.com', desc:'Quick Response code. Two-dimensional symbology that encodes large amounts of text, URLs, contacts, or binary data.', spec:['Up to 4 KB','Error correction','Ubiquitous'] },
{ id:'dm', name:'Data Matrix', group:'2D · Matrix', engine:'bwip', code:'datamatrix', example:'DataMatrix-Example-2026', desc:'High-density 2D symbology used in electronics, automotive, and aerospace manufacturing.', spec:['Up to 2335 chars','Square / rect','Industrial'] },
{ id:'pdf417', name:'PDF417', group:'2D · Matrix', engine:'bwip', code:'pdf417', example:'PDF417 multi-row stacked symbology', desc:'Stacked linear 2D barcode used on ID cards, boarding passes, and shipping labels.', spec:['Up to 1850 chars','Stacked rows','ID & travel'] },
];
/* =========================================================
State
========================================================= */
let currentFormat = FORMATS[0];
const generated = []; // {format, data, canvas, error, showText}
/* =========================================================
Render sidebar
========================================================= */
function renderSidebar(){
const list = document.getElementById('formatList');
document.getElementById('formatCount').textContent = `${FORMATS.length} FORMATS`;
const groups = {};
FORMATS.forEach(f => { (groups[f.group] = groups[f.group] || []).push(f); });
let html = '';
for (const g of Object.keys(groups)){
html += `<div class="group"><div class="group-title">${g}</div>`;
for (const f of groups[g]){
html += `<div class="fmt" data-id="${f.id}">
<span class="fmt-name">${f.name}</span>
<span class="fmt-code">${f.id}</span>
</div>`;
}
html += '</div>';
}
list.innerHTML = html;
list.querySelectorAll('.fmt').forEach(el => {
el.addEventListener('click', () => {
const fmt = FORMATS.find(f => f.id === el.dataset.id);
selectFormat(fmt);
});
});
}
/* =========================================================
Select format & show example
========================================================= */
function selectFormat(fmt){
currentFormat = fmt;
document.querySelectorAll('.fmt').forEach(el => el.classList.toggle('active', el.dataset.id === fmt.id));
document.getElementById('selName').textContent = fmt.name;
document.getElementById('selType').textContent = fmt.group;
document.getElementById('selDesc').textContent = fmt.desc;
const specHtml = (fmt.spec || []).map(s => `<span><b>·</b> ${s}</span>`).join('');
document.getElementById('selSpec').innerHTML = specHtml;
// Render example
const wrap = document.getElementById('exampleWrap');
wrap.innerHTML = '';
renderBarcode(fmt, fmt.example, wrap, { exampleMode:true })
.then(() => {
const cap = document.createElement('div');
cap.style.cssText = 'font-size:10px;color:var(--ink-soft);letter-spacing:.08em;text-align:center;margin-top:4px;word-break:break-all';
cap.innerHTML = `<b style="color:var(--ink)">Sample data:</b> ${escapeHtml(fmt.example)}`;
wrap.appendChild(cap);
})
.catch(err => {
wrap.innerHTML = `<div class="empty">Example unavailable</div>`;
});
}
/* =========================================================
Core barcode renderer — returns Promise<HTMLCanvasElement>
========================================================= */
function renderBarcode(fmt, data, container, opts = {}){
return new Promise((resolve, reject) => {
const scale = opts.exampleMode ? 2 : parseInt(document.getElementById('optScale').value || 3, 10);
const showText = (typeof opts.showText === 'boolean')
? opts.showText
: document.getElementById('optShowText').checked;
const canvas = document.createElement('canvas');
try {
if (fmt.engine === 'jsbarcode') {
let codeData = data;
// ISBN: strip any prefix and use EAN13 underneath
if (fmt.id === 'isbn'){
codeData = data.replace(/[-\s]/g,'');
if (codeData.length === 10) {
// convert ISBN-10 to ISBN-13
const base = '978' + codeData.slice(0,9);
let sum = 0;
for (let i=0;i<12;i++) sum += (i%2===0 ? 1 : 3) * parseInt(base[i],10);
const check = (10 - (sum%10)) % 10;
codeData = base + check;
}
}
JsBarcode(canvas, codeData, {
format: fmt.code,
width: scale * 0.8,
height: opts.exampleMode ? 60 : 80,
displayValue: showText,
font: 'JetBrains Mono',
fontSize: opts.exampleMode ? 12 : 14,
margin: 8,
background: '#ffffff',
lineColor: '#14110d'
});
container.appendChild(canvas);
resolve(canvas);
return;
}
if (fmt.engine === 'bwip') {
// bwip-js renders to canvas
const opts2 = {
bcid: fmt.code,
text: data,
scale: opts.exampleMode ? 2 : scale,
height: 10,
includetext: showText,
textxalign: 'center',
backgroundcolor: 'FFFFFF',
textfont: 'JetBrains Mono',
paddingwidth: 4,
paddingheight: 4,
};
// 2D codes don't take height the same way
if (['datamatrix','qrcode','pdf417'].includes(fmt.code)) {
delete opts2.height;
}
bwipjs.toCanvas(canvas, opts2);
container.appendChild(canvas);
resolve(canvas);
return;
}
reject(new Error('Unknown engine'));
} catch (err) {
reject(err);
}
});
}
/* =========================================================
Generate all from textarea
========================================================= */
function generateAll(){
const raw = document.getElementById('dataInput').value;
const lines = raw.split('\n').map(l => l.trim()).filter(l => l.length > 0);
if (lines.length === 0){
toast('Enter at least one line of data');
return;
}
const resultsEl = document.getElementById('results');
resultsEl.innerHTML = '';
generated.length = 0;
const grid = document.createElement('div');
grid.className = 'results-grid';
resultsEl.appendChild(grid);
let successCount = 0;
const globalShowText = document.getElementById('optShowText').checked;
const promises = lines.map((line, idx) => {
const card = document.createElement('div');
card.className = 'result-card';
card.innerHTML = `
<span class="idx">#${String(idx+1).padStart(2,'0')}</span>
<div class="result-canvas-wrap" data-slot="${idx}"></div>
<div class="result-data"><b>${escapeHtml(currentFormat.name)}</b> · ${escapeHtml(line)}</div>
<div class="result-actions">
<button data-act="toggle" data-idx="${idx}" title="Show/hide text below barcode">${globalShowText ? 'Hide text' : 'Show text'}</button>
<button data-act="jpg" data-idx="${idx}">JPG</button>
<button data-act="png" data-idx="${idx}">PNG</button>
</div>
`;
grid.appendChild(card);
const slot = card.querySelector('.result-canvas-wrap');
return renderBarcode(currentFormat, line, slot, { showText: globalShowText })
.then(canvas => {
generated[idx] = { format: currentFormat, data: line, canvas, error:null, showText: globalShowText };
successCount++;
})
.catch(err => {
slot.innerHTML = `<div class="result-error">⚠ ${escapeHtml(err.message || String(err))}<br><small>Check that your data matches the format requirements.</small></div>`;
card.querySelector('.result-actions').style.display = 'none';
generated[idx] = { format: currentFormat, data: line, canvas:null, error: err, showText: globalShowText };
});
});
Promise.all(promises).then(() => {
document.getElementById('resultsStats').innerHTML = `Generated · <b>${successCount}</b> / ${lines.length} · Format <b>${currentFormat.name}</b>`;
// Wire action buttons
grid.querySelectorAll('button[data-act]').forEach(b => {
b.addEventListener('click', () => {
const idx = parseInt(b.dataset.idx, 10);
const act = b.dataset.act;
if (act === 'toggle') {
toggleCardText(idx);
} else {
downloadOne(idx, act);
}
});
});
if (successCount > 0) toast(`Generated ${successCount} barcode${successCount>1?'s':''}`);
});
}
/* =========================================================
Download helpers
========================================================= */
function canvasToBlob(canvas, type, quality){
return new Promise(resolve => canvas.toBlob(resolve, type, quality));
}
function safeName(s){ return s.replace(/[^a-zA-Z0-9_-]/g,'_').slice(0,40) || 'barcode'; }
function toggleCardText(idx){
const item = generated[idx];
if (!item || item.error) return;
item.showText = !item.showText;
// Find the card via the slot's data attribute
const slot = document.querySelector(`.result-canvas-wrap[data-slot="${idx}"]`);
if (!slot) return;
slot.innerHTML = '';
renderBarcode(item.format, item.data, slot, { showText: item.showText })
.then(canvas => {
item.canvas = canvas;
const btn = document.querySelector(`button[data-act="toggle"][data-idx="${idx}"]`);
if (btn) btn.textContent = item.showText ? 'Hide text' : 'Show text';
});
}
async function downloadOne(idx, ext){
const item = generated[idx];
if (!item || !item.canvas) return;
const quality = parseFloat(document.getElementById('optQuality').value || 0.95);
if (ext === 'jpg'){
// Convert to JPG with white background (canvases may have alpha)
const c = document.createElement('canvas');
c.width = item.canvas.width;
c.height = item.canvas.height;
const ctx = c.getContext('2d');
ctx.fillStyle = '#ffffff';
ctx.fillRect(0,0,c.width,c.height);
ctx.drawImage(item.canvas, 0, 0);
const blob = await canvasToBlob(c, 'image/jpeg', quality);
saveBlob(blob, `${safeName(item.format.id+'_'+item.data)}.jpg`);
} else {
const blob = await canvasToBlob(item.canvas, 'image/png');
saveBlob(blob, `${safeName(item.format.id+'_'+item.data)}.png`);
}
}
function saveBlob(blob, filename){
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url; a.download = filename;
document.body.appendChild(a); a.click(); a.remove();
setTimeout(() => URL.revokeObjectURL(url), 1000);
}
async function downloadAll(){
const items = generated.filter(g => g && g.canvas);
if (items.length === 0){ toast('Nothing to download — generate first'); return; }
for (let i=0;i<items.length;i++){
await downloadOne(generated.indexOf(items[i]), 'jpg');
await new Promise(r => setTimeout(r, 120));
}
toast(`Downloaded ${items.length} JPG${items.length>1?'s':''}`);
}
/* =========================================================
UI helpers
========================================================= */
function escapeHtml(s){ return String(s).replace(/[&<>"']/g, c => ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#39;'}[c])); }
let toastTimer = null;
function toast(msg){
const t = document.getElementById('toast');
t.textContent = msg;
t.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => t.classList.remove('show'), 2200);
}
/* =========================================================
Wire up
========================================================= */
document.getElementById('btnGenerate').addEventListener('click', generateAll);
document.getElementById('btnDownloadAll').addEventListener('click', downloadAll);
document.getElementById('btnClear').addEventListener('click', () => {
document.getElementById('dataInput').value = '';
document.getElementById('results').innerHTML = '<div class="results-empty">No barcodes generated yet</div>';
document.getElementById('resultsStats').innerHTML = 'Generated · <b>0</b>';
generated.length = 0;
});
// Re-render example when options change
['optShowText','optScale'].forEach(id => {
document.getElementById(id).addEventListener('change', () => selectFormat(currentFormat));
});
// Quick keyboard shortcut: Ctrl/Cmd + Enter to generate
document.getElementById('dataInput').addEventListener('keydown', (e) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter'){
e.preventDefault();
generateAll();
}
});
// Init
renderSidebar();
selectFormat(FORMATS[0]);
// Preload example data in textarea
document.getElementById('dataInput').value = '5901234123457\n4006381333931\n9780201379624';
</script>
</body>
</html>