Files
Barcode/web/index.html
T
2026-09-13 20:05:01 +01:00

795 lines
35 KiB
HTML
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.
<!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>