Updating index.php to the real one.

This commit is contained in:
2026-02-06 16:03:07 -06:00
parent c7d69ee7cb
commit baa9e7d60c
3 changed files with 830 additions and 830 deletions

216
archive/index.php Normal file
View File

@@ -0,0 +1,216 @@
<?php
// Databrae (working name)
// Proof of concept
$autoloadOk = file_exists(__DIR__ . '/vendor/autoload.php');
if ($autoloadOk) {
require __DIR__ . '/vendor/autoload.php';
}
// Using Tecnick's barcode library to generate data matrix barcodes
use Com\Tecnick\Barcode\Barcode;
function safe($s) {
return htmlspecialchars(trim($s ?? ''), ENT_QUOTES, 'UTF-8');
}
// Set up the form input
$data1 = $_POST['data1'] ?? '';
$data2 = $_POST['data2'] ?? '';
$data3 = $_POST['data3'] ?? '';
$data4 = $_POST['data4'] ?? '';
$barcodeText = $_POST['barcode'] ?? '';
$isPosted = ($_SERVER['REQUEST_METHOD'] === 'POST');
// Declare some variables
$svgBarcode = '';
$errorMsg = '';
// Generate the data matrix barcode and handle some common errors where I've already screwed up
if ($isPosted) {
if (!$autoloadOk) {
$errorMsg = 'Composer autoload not found. Run <code>composer require tecnickcom/tc-lib-barcode</code> in this folder.';
} elseif ($barcodeText === '') {
$errorMsg = 'Please provide a Barcode value.';
} else {
try {
$barcode = new Barcode();
// Create the data matrix as an SVG
$bobj = $barcode->getBarcodeObj(
'DATAMATRIX',
$barcodeText,
-1, // width auto
-1, // height auto
'black',
[0, 0, 0, 0]
);
$bobj->setBackgroundColor('white');
$svgBarcode = $bobj->getSvgCode();
} catch (Throwable $e) {
$errorMsg = 'Barcode generation error: ' . safe($e->getMessage());
}
}
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Spine Label Generator</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--font: "Helvetica Neue", Arial, sans-serif;
}
body {
font-family: var(--font);
margin: 1.5rem;
color: #222;
line-height: 1.35;
}
form {
display: grid;
gap: .75rem;
max-width: 520px;
margin-bottom: 1.5rem;
}
label { font-weight: 600; }
input[type="text"] {
padding: .5rem .6rem;
border: 1px solid #bbb;
border-radius: 6px;
font-size: 1rem;
width: 100%;
}
.row { display: grid; gap: .35rem; }
.actions { margin-top: .5rem; }
button {
padding: .6rem .9rem;
border: 1px solid #444;
background: #111;
color: #fff;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
button:hover { filter: brightness(1.05); }
.note {
font-size: .9rem;
color: #555;
}
.error {
padding: .75rem 1rem;
border: 1px solid #d33;
background: #fee;
color: #a00;
border-radius: 8px;
max-width: 520px;
margin-bottom: 1rem;
}
/* ===== Label (exact print size) ===== */
.preview-wrap {
display: grid;
gap: 1rem;
}
.label-card {
width: 2.5cm; /* fixed physical width */
height: 3.5cm; /* fixed physical height */
box-sizing: border-box; /* include padding in the fixed size */
border: 1px dashed #aaa;/* guide border (hidden on print) */
padding: 0.2cm; /* compact padding to fit content */
display: flex;
flex-direction: column;
justify-content: space-between; /* push barcode to bottom */
background: #fff;
}
.label-text {
display: grid;
gap: 0.1cm; /* tight vertical spacing between Data 13 */
text-align: center; /* center the three lines */
margin: .2cm;
line-height: 1.2;
font-size: 0.35cm; /* ~8pt—adjust as needed */
word-break: break-word;
}
.label-text-line { white-space: pre-wrap; }
.barcode-block {
display: flex;
justify-content: center; /* center code horizontally */
align-items: flex-end;
margin-top: 0.05cm; /* tiny breathing room under Data 3 */
}
/* Keep the Data Matrix exactly 1.5 cm in height; width auto to keep it square */
.barcode-block svg {
display: block;
height: .5cm; /* requested fixed height */
width: auto;
}
@media print {
form, .note, .error, h1 { display: none !important; }
body { margin: 0; }
.label-card { border: none; } /* remove guide border on print */
/* Ensure the browser doesn't scale */
@page { margin: 0; }
}
</style>
</head>
<body>
<h1>Spine Label Generator</h1>
<form method="post">
<div class="row">
<label for="data1">Data 1</label>
<input type="text" id="data1" name="data1" value="<?= safe($data1) ?>" placeholder="e.g., QA76.73.P224">
</div>
<div class="row">
<label for="data2">Data 2</label>
<input type="text" id="data2" name="data2" value="<?= safe($data2) ?>" placeholder="e.g., 2025">
</div>
<div class="row">
<label for="data3">Data 3</label>
<input type="text" id="data3" name="data3" value="<?= safe($data3) ?>" placeholder="e.g., v.2">
</div>
<div class="row">
<label for="data4">Data 4</label>
<input type="text" id="data4" name="data4" value="<?= safe($_POST['data4'] ?? '') ?>" placeholder="e.g., Copy 1">
</div>
<div class="row">
<label for="barcode">Barcode (alphanumeric)</label>
<input type="text" id="barcode" name="barcode" value="<?= safe($barcodeText) ?>" placeholder="e.g., 1230004231819">
</div>
<div class="actions">
<button type="submit">Build Label</button>
</div>
<p class="note">Tip: In the browser print dialog, set scale to <strong>100% / Actual size</strong>.</p>
</form>
<?php if ($errorMsg): ?>
<div class="error"><?= $errorMsg ?></div>
<?php endif; ?>
<?php if ($isPosted && !$errorMsg): ?>
<div class="preview-wrap">
<div class="label-card">
<div class="label-text">
<div class="label-text-line"><?= nl2br(safe($data1)) ?></div>
<div class="label-text-line"><?= nl2br(safe($data2)) ?></div>
<div class="label-text-line"><?= nl2br(safe($data3)) ?></div>
<div class="label-text-line"><?= nl2br(safe($_POST['data4'] ?? '')) ?></div>
</div>
<div class="barcode-block">
<?= $svgBarcode /* Inline SVG keeps print quality high */ ?>
</div>
</div>
</div>
<?php endif; ?>
</body>
</html>

View File

@@ -1,615 +0,0 @@
<?php
// index.php — Spine/Item Label Generator with CSV + Sheet Template (Demco 1.5"x1.0")
// Prints to exact physical size using CSS inches and @page rules.
// Preset defaults can be adjusted from the UI without code changes.
declare(strict_types=1);
$autoloadOk = file_exists(__DIR__ . '/vendor/autoload.php');
if ($autoloadOk) {
require __DIR__ . '/vendor/autoload.php';
}
use Com\Tecnick\Barcode\Barcode;
function safe($s) { return htmlspecialchars(trim((string)($s ?? '')), ENT_QUOTES, 'UTF-8'); }
function strip_bom(string $s): string {
if (substr($s, 0, 3) === "\xEF\xBB\xBF") return substr($s, 3);
return $s;
}
function detect_delimiter(string $headerLine): string {
$candidates = [",", "\t", ";", "|"];
$counts = [];
foreach ($candidates as $d) $counts[$d] = substr_count($headerLine, $d);
arsort($counts);
return array_key_first($counts) ?: ",";
}
function read_csv_assoc(string $tmpPath, array &$errors): array {
$rows = [];
$required = ['data1','data2','data3','data4','barcode'];
$raw = file_get_contents($tmpPath);
if ($raw === false || $raw === '') { $errors[] = "Uploaded CSV is empty or unreadable."; return []; }
$raw = strip_bom($raw);
$lines = preg_split('/\r\n|\r|\n/', $raw);
if (!$lines || count($lines) < 1) { $errors[] = "CSV appears to have no lines."; return []; }
$headerLine = '';
foreach ($lines as $ln) { if (trim($ln) !== '') { $headerLine = $ln; break; } }
if ($headerLine === '') { $errors[] = "CSV header line is missing."; return []; }
$delimiter = detect_delimiter($headerLine);
$fh = fopen($tmpPath, 'r'); if (!$fh) { $errors[] = "Unable to open uploaded CSV."; return []; }
// Read header
$hdr = [];
while (($line = fgets($fh)) !== false) {
$line = strip_bom($line);
if (trim($line) === '') continue;
$hdr = str_getcsv($line, $delimiter);
break;
}
if (!$hdr) { $errors[] = "Could not read header row."; fclose($fh); return []; }
$headers = array_map(fn($h) => strtolower(trim((string)$h)), $hdr);
foreach (['data1','data2','data3','data4','barcode'] as $req) {
if (!in_array($req, $headers, true)) $errors[] = "Missing required header: <code>{$req}</code>.";
}
if ($errors) { fclose($fh); return []; }
while (($row = fgetcsv($fh, 0, $delimiter)) !== false) {
if ($row === [null] || $row === false) continue;
if (count(array_filter($row, fn($v) => trim((string)$v) !== '')) === 0) continue; // skip blank line
$assoc = [];
foreach ($required as $key) {
$idx = array_search($key, $headers, true);
$assoc[$key] = $idx !== false && isset($row[$idx]) ? trim((string)$row[$idx]) : '';
}
if (implode('', $assoc) === '') continue;
$rows[] = $assoc;
}
fclose($fh);
if (empty($rows)) $errors[] = "CSV contained no usable rows after the header.";
return $rows;
}
function render_datamatrix_svg(string $content): string {
global $autoloadOk;
if (!$autoloadOk) return '<!-- Barcode library not installed -->';
$barcode = new Barcode();
$obj = $barcode->getBarcodeObj(
'DATAMATRIX',
$content,
-1, -1, 'black', [0,0,0,0]
);
$obj->setBackgroundColor('white');
return $obj->getSvgCode();
}
/* ---------------- Sheet Template System ---------------- */
// Default preset: Demco 1.5" x 1.0" on Letter (guess: 5x8 grid, 0.5" margins, 0.125" gaps).
// You can adjust these in the UI and re-preview until alignment is perfect.
$defaults = [
'page_width_in' => 8.5,
'page_height_in' => 11.0,
'margin_top_in' => 0.5,
'margin_right_in' => 0.5,
'margin_bottom_in' => 0.5,
'margin_left_in' => 0.5,
'cols' => 5,
'rows' => 8,
'label_w_in' => 1.5,
'label_h_in' => 1.0,
'gap_h_in' => 0.125, // horizontal gap between columns
'gap_v_in' => 0.125, // vertical gap between rows
'barcode_h_in' => 0.30, // barcode height inside 1.0" label
'font_in' => 0.12, // approx 8.6pt
'show_outlines' => '1'
];
// Pull current template params from POST (template form) or keep defaults
function valf($name, $def) {
return isset($_POST[$name]) && $_POST[$name] !== '' ? $_POST[$name] : $def;
}
$page_width_in = (float) valf('page_width_in', $defaults['page_width_in']);
$page_height_in = (float) valf('page_height_in', $defaults['page_height_in']);
$margin_top_in = (float) valf('margin_top_in', $defaults['margin_top_in']);
$margin_right_in = (float) valf('margin_right_in', $defaults['margin_right_in']);
$margin_bottom_in = (float) valf('margin_bottom_in', $defaults['margin_bottom_in']);
$margin_left_in = (float) valf('margin_left_in', $defaults['margin_left_in']);
$cols = max(1, (int) valf('cols', $defaults['cols']));
$rows = max(1, (int) valf('rows', $defaults['rows']));
$label_w_in = (float) valf('label_w_in', $defaults['label_w_in']);
$label_h_in = (float) valf('label_h_in', $defaults['label_h_in']);
$gap_h_in = (float) valf('gap_h_in', $defaults['gap_h_in']);
$gap_v_in = (float) valf('gap_v_in', $defaults['gap_v_in']);
$barcode_h_in = (float) valf('barcode_h_in', $defaults['barcode_h_in']);
$font_in = (float) valf('font_in', $defaults['font_in']);
$show_outlines = isset($_POST['show_outlines']) ? '1' : $defaults['show_outlines'];
// Records pipeline (single or CSV)
$errors = [];
$records = [];
$isPosted = ($_SERVER['REQUEST_METHOD'] === 'POST');
$data1 = $_POST['data1'] ?? '';
$data2 = $_POST['data2'] ?? '';
$data3 = $_POST['data3'] ?? '';
$data4 = $_POST['data4'] ?? '';
$barcodeText = $_POST['barcode'] ?? '';
$mode = $_POST['mode'] ?? '';
if ($isPosted && $mode === 'csv') {
if (!isset($_FILES['csvfile']) || !is_uploaded_file($_FILES['csvfile']['tmp_name'])) {
$errors[] = "Please choose a CSV file to upload.";
} else {
$size = $_FILES['csvfile']['size'] ?? 0;
if ($size > 10 * 1024 * 1024) { $errors[] = "CSV too large (max 10MB)."; }
else { $records = read_csv_assoc($_FILES['csvfile']['tmp_name'], $errors); }
}
} elseif ($isPosted && $mode === 'single') {
if ($barcodeText === '') $errors[] = "Please provide a Barcode value.";
else {
$records[] = ['data1'=>$data1,'data2'=>$data2,'data3'=>$data3,'data4'=>$data4,'barcode'=>$barcodeText];
}
}
// Single-label physical dimensions (as fed by the label printer: 3.7cm wide × 2.5cm tall)
// Content is rotated 90° so it reads correctly when applied to a spine.
$single_feed_w_cm = 3.7;
$single_feed_h_cm = 2.5;
// After rotation, the visible label area is 2.5cm wide × 3.7cm tall
$single_label_w_cm = 2.5;
$single_label_h_cm = 3.7;
$is_single = ($mode === 'single' && !empty($records));
// Compute pagination (CSV/sheet mode only)
$perPage = $cols * $rows;
$pages = [];
if ($records && !$is_single) {
$pages = array_chunk($records, $perPage);
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Label Generator — CSV + Sheet Template</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--font: "Helvetica Neue", Arial, sans-serif;
/* These CSS variables are set from PHP inline below to match your template */
--page-w: <?= $page_width_in ?>in;
--page-h: <?= $page_height_in ?>in;
--m-top: <?= $margin_top_in ?>in;
--m-right: <?= $margin_right_in ?>in;
--m-bottom: <?= $margin_bottom_in ?>in;
--m-left: <?= $margin_left_in ?>in;
--cols: <?= $cols ?>;
--rows: <?= $rows ?>;
--label-w: <?= $label_w_in ?>in;
--label-h: <?= $label_h_in ?>in;
--gap-h: <?= $gap_h_in ?>in;
--gap-v: <?= $gap_v_in ?>in;
--barcode-h: <?= $barcode_h_in ?>in;
--font-in: <?= $font_in ?>in;
--show-outlines: <?= $show_outlines === '1' ? 1 : 0 ?>;
}
<?php if ($is_single): ?>
@page {
size: <?= $single_feed_w_cm ?>cm <?= $single_feed_h_cm ?>cm landscape;
margin: 0;
}
<?php else: ?>
@page {
size: var(--page-w) var(--page-h);
margin: 0; /* we handle margins inside the .sheet via padding */
}
<?php endif; ?>
body {
font-family: var(--font);
margin: 1.5rem;
color: #222;
line-height: 1.35;
}
h1 { margin-bottom: .25rem; }
.subtle { color: #666; margin-top: 0; }
.wrap {
display: grid;
gap: 1.25rem;
max-width: 1200px;
}
.pane {
border: 1px solid #ddd;
border-radius: 10px;
padding: 1rem;
}
.pane h2 { margin: 0 0 .75rem; font-size: 1.1rem; }
form { display: grid; gap: .75rem; }
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .75rem 1rem;
}
label { font-weight: 600; }
input[type="text"], input[type="number"], input[type="file"] {
padding: .5rem .6rem;
border: 1px solid #bbb;
border-radius: 6px;
font-size: 1rem;
width: 100%;
background: #fff;
}
.actions { display: flex; gap: .5rem; flex-wrap: wrap; }
button {
padding: .6rem .9rem;
border: 1px solid #444;
background: #111; color: #fff;
border-radius: 8px; cursor: pointer; font-weight: 600;
}
.ghost { background: #f7f7f7; color: #222; border-color: #ccc; }
.info { font-size: .9rem; color: #555; }
.error {
padding: .75rem 1rem; border: 1px solid #d33; background: #fee; color: #a00;
border-radius: 8px; margin-bottom: .75rem;
}
/* ====== SHEET LAYOUT ====== */
.sheets { display: grid; gap: 1rem; }
.sheet {
width: var(--page-w);
height: var(--page-h);
padding: var(--m-top) var(--m-right) var(--m-bottom) var(--m-left);
box-sizing: border-box;
background: white;
position: relative;
page-break-after: always;
}
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(<?= $cols ?>, var(--label-w));
grid-template-rows: repeat(<?= $rows ?>, var(--label-h));
column-gap: var(--gap-h);
row-gap: var(--gap-v);
justify-content: start; /* no stretching */
align-content: start;
}
.cell {
width: var(--label-w);
height: var(--label-h);
box-sizing: border-box;
padding: 0.06in; /* small inner padding for aesthetics; adjust if needed */
display: flex;
flex-direction: column;
justify-content: space-between;
background: #fff;
border: calc(var(--show-outlines) * 1px) dashed #999; /* outline toggle for test prints */
}
.label-text {
display: grid;
gap: 0.04in;
text-align: center;
margin: 0;
line-height: 1.2;
font-size: var(--font-in);
word-break: break-word;
}
.label-text-line { white-space: pre-wrap; }
.barcode-block { display: flex; justify-content: center; align-items: flex-end; margin-top: 0.02in; }
.barcode-block svg { display: block; height: var(--barcode-h); width: auto; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
/* ====== SINGLE LABEL (standalone) ====== */
.single-label-wrap {
display: flex;
justify-content: center;
padding: 1rem 0;
}
.single-label-feed {
/* Outer box matches the physical feed size (3.7cm × 2.5cm landscape) */
width: <?= $single_feed_w_cm ?>cm;
height: <?= $single_feed_h_cm ?>cm;
overflow: hidden;
background: #fff;
border: 1px dashed #999;
position: relative;
}
.single-label {
/* Inner content sized as portrait (2.5cm × 3.7cm), then rotated to fit the landscape feed */
width: <?= $single_label_w_cm ?>cm;
height: <?= $single_label_h_cm ?>cm;
box-sizing: border-box;
padding: 0.2cm;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 11pt;
/* Rotate 90° clockwise and re-center within the feed box */
transform: rotate(90deg);
transform-origin: center center;
position: absolute;
top: 50%;
left: 50%;
margin-top: calc(-<?= $single_label_h_cm ?>cm / 2);
margin-left: calc(-<?= $single_label_w_cm ?>cm / 2);
}
.single-label .label-text {
display: grid;
gap: 0.04cm;
text-align: center;
margin: 0;
line-height: 1.2;
font-size: inherit;
word-break: break-word;
}
.single-label .barcode-block {
display: flex;
justify-content: center;
align-items: flex-end;
margin-top: 0.02cm;
}
.single-label .barcode-block svg {
display: block;
height: 0.8cm;
width: auto;
}
@media print {
.pane, .controls, .error, .info, .subtle, h1, .wrap { display: none !important; }
body { margin: 0; padding: 0; background: white; }
.sheet { border: none; }
/* Single label: fill the page exactly */
.single-label-wrap { display: flex !important; padding: 0; margin: 0; justify-content: start; }
.single-label-feed {
width: <?= $single_feed_w_cm ?>cm;
height: <?= $single_feed_h_cm ?>cm;
border: none;
margin: 0;
}
/* Sheet mode: show sheets */
.sheets { display: grid !important; }
}
</style>
<script>
function goPrint(){ window.print(); }
function jump(id){ document.getElementById(id)?.scrollIntoView({behavior:'smooth'}); }
</script>
</head>
<body>
<h1>Label Generator</h1>
<p class="subtle">Import CSV and print to a **1.5" × 1.0"** template (Demco) at <strong>100% / Actual size</strong>.</p>
<div class="wrap">
<!-- Single label form -->
<div class="pane">
<h2>Single Label</h2>
<form method="post">
<input type="hidden" name="mode" value="single">
<!-- Preserve template params across submissions -->
<?php foreach ([
'page_width_in','page_height_in','margin_top_in','margin_right_in','margin_bottom_in','margin_left_in',
'cols','rows','label_w_in','label_h_in','gap_h_in','gap_v_in','barcode_h_in','font_in'
] as $p): ?>
<input type="hidden" name="<?= $p ?>" value="<?= safe($_POST[$p] ?? $defaults[$p] ?? '') ?>">
<?php endforeach; ?>
<?php if ($show_outlines === '1'): ?><input type="hidden" name="show_outlines" value="1"><?php endif; ?>
<div class="grid2">
<div>
<label for="data1">Data 1</label>
<input type="text" id="data1" name="data1" value="<?= safe($data1) ?>" placeholder="e.g., QA76.73.P224">
</div>
<div>
<label for="data2">Data 2</label>
<input type="text" id="data2" name="data2" value="<?= safe($data2) ?>" placeholder="e.g., 2025">
</div>
<div>
<label for="data3">Data 3</label>
<input type="text" id="data3" name="data3" value="<?= safe($data3) ?>" placeholder="e.g., v.2">
</div>
<div>
<label for="data4">Data 4</label>
<input type="text" id="data4" name="data4" value="<?= safe($data4) ?>" placeholder="e.g., Copy 1">
</div>
<div style="grid-column: 1 / -1;">
<label for="barcode">Barcode (alphanumeric)</label>
<input type="text" id="barcode" name="barcode" value="<?= safe($barcodeText) ?>" placeholder="e.g., 1230004231819">
</div>
</div>
<div class="actions">
<button type="submit">Preview Single Label</button>
<button type="button" class="ghost" onclick="jump('csvpane')">Or Import CSV…</button>
</div>
<p class="info">Data Matrix prints as SVG at <strong><?= number_format($barcode_h_in, 2) ?> in</strong> height.</p>
</form>
</div>
<!-- CSV import -->
<div class="pane" id="csvpane">
<h2>Import CSV (data1,data2,data3,data4,barcode)</h2>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="mode" value="csv">
<!-- persist template params -->
<div class="grid2">
<div style="grid-column: 1 / -1;">
<label for="csvfile">Choose CSV file</label>
<input type="file" id="csvfile" name="csvfile" accept=".csv,text/csv">
</div>
</div>
<?php foreach ([
'page_width_in','page_height_in','margin_top_in','margin_right_in','margin_bottom_in','margin_left_in',
'cols','rows','label_w_in','label_h_in','gap_h_in','gap_v_in','barcode_h_in','font_in'
] as $p): ?>
<input type="hidden" name="<?= $p ?>" value="<?= safe($_POST[$p] ?? $defaults[$p] ?? '') ?>">
<?php endforeach; ?>
<?php if ($show_outlines === '1'): ?><input type="hidden" name="show_outlines" value="1"><?php endif; ?>
<div class="actions">
<button type="submit">Upload & Preview Labels</button>
<button type="reset" class="ghost">Reset</button>
</div>
<p class="info">Delimiters (comma, tab, semicolon) auto-detected. Headers must match exactly.</p>
</form>
</div>
<!-- Template settings -->
<div class="pane">
<h2>Template (Demco 1.5" × 1.0")</h2>
<form method="post">
<div class="grid2">
<div><label>Page width (in)</label><input type="number" step="0.01" name="page_width_in" value="<?= safe($page_width_in) ?>"></div>
<div><label>Page height (in)</label><input type="number" step="0.01" name="page_height_in" value="<?= safe($page_height_in) ?>"></div>
<div><label>Margin top (in)</label><input type="number" step="0.01" name="margin_top_in" value="<?= safe($margin_top_in) ?>"></div>
<div><label>Margin right (in)</label><input type="number" step="0.01" name="margin_right_in" value="<?= safe($margin_right_in) ?>"></div>
<div><label>Margin bottom (in)</label><input type="number" step="0.01" name="margin_bottom_in" value="<?= safe($margin_bottom_in) ?>"></div>
<div><label>Margin left (in)</label><input type="number" step="0.01" name="margin_left_in" value="<?= safe($margin_left_in) ?>"></div>
<div><label>Columns</label><input type="number" step="1" min="1" name="cols" value="<?= safe($cols) ?>"></div>
<div><label>Rows</label><input type="number" step="1" min="1" name="rows" value="<?= safe($rows) ?>"></div>
<div><label>Label width (in)</label><input type="number" step="0.01" name="label_w_in" value="<?= safe($label_w_in) ?>"></div>
<div><label>Label height (in)</label><input type="number" step="0.01" name="label_h_in" value="<?= safe($label_h_in) ?>"></div>
<div><label>Horizontal gap (in)</label><input type="number" step="0.01" name="gap_h_in" value="<?= safe($gap_h_in) ?>"></div>
<div><label>Vertical gap (in)</label><input type="number" step="0.01" name="gap_v_in" value="<?= safe($gap_v_in) ?>"></div>
<div><label>Barcode height (in)</label><input type="number" step="0.01" name="barcode_h_in" value="<?= safe($barcode_h_in) ?>"></div>
<div><label>Font size (in)</label><input type="number" step="0.01" name="font_in" value="<?= safe($font_in) ?>"></div>
<div style="grid-column: 1 / -1; display:flex; align-items:center; gap:.5rem;">
<input type="checkbox" id="show_outlines" name="show_outlines" value="1" <?= $show_outlines==='1'?'checked':'' ?>>
<label for="show_outlines">Show label outlines (for alignment/test prints)</label>
</div>
</div>
<!-- Keep any uploaded/entered data visible on re-submit by passing the last single entry (optional) -->
<input type="hidden" name="mode" value="<?= safe($mode ?: 'single') ?>">
<input type="hidden" name="data1" value="<?= safe($data1) ?>">
<input type="hidden" name="data2" value="<?= safe($data2) ?>">
<input type="hidden" name="data3" value="<?= safe($data3) ?>">
<input type="hidden" name="data4" value="<?= safe($data4) ?>">
<input type="hidden" name="barcode" value="<?= safe($barcodeText) ?>">
<div class="actions">
<button type="submit">Apply Template Settings</button>
<button type="button" class="ghost" onclick="goPrint()">Print</button>
</div>
<p class="info">
<strong>Tip:</strong> Do a test print on plain paper with “Show label outlines” checked.
Hold it behind a real sheet to check alignment, then tweak margins/gaps as needed.
</p>
</form>
</div>
<!-- Errors -->
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Issues:</strong>
<ul><?php foreach ($errors as $e): ?><li><?= $e ?></li><?php endforeach; ?></ul>
</div>
<?php endif; ?>
<!-- Preview / Sheets (CSV mode) -->
<?php if ($records && !$errors && !$is_single): ?>
<div class="controls">
<button onclick="goPrint()">Print</button>
<span class="info"><?= count($records) ?> label(s), <?= count($pages) ?> sheet(s) @ <?= $cols ?>×<?= $rows ?>.</span>
</div>
<div class="sheets">
<?php foreach ($pages as $pageIndex => $pageRecs): ?>
<div class="sheet">
<div class="grid">
<?php
// Fill cells row-major: put records first, blank cells if fewer than capacity.
$count = count($pageRecs);
$capacity = $cols * $rows;
for ($i = 0; $i < $capacity; $i++):
$rec = $pageRecs[$i] ?? null;
?>
<div class="cell">
<?php if ($rec): ?>
<div class="label-text">
<div class="label-text-line"><?= nl2br(safe($rec['data1'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data2'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data3'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data4'] ?? '')) ?></div>
</div>
<div class="barcode-block">
<?= render_datamatrix_svg((string)($rec['barcode'] ?? '')) ?>
</div>
<?php endif; ?>
</div>
<?php endfor; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Single label preview -->
<?php if ($is_single && !$errors):
$rec = $records[0];
?>
<div class="controls">
<button onclick="goPrint()">Print Label</button>
<span class="info">Single label — <?= $single_label_w_cm ?>cm × <?= $single_label_h_cm ?>cm</span>
</div>
<?php endif; ?>
<?php if (!$autoloadOk): ?>
<div class="error">
Composer autoload not found. Install the barcode library with:<br>
<code>composer require tecnickcom/tc-lib-barcode</code><br>
(Make sure BCMath is enabled.)
</div>
<?php endif; ?>
</div>
<?php if ($is_single && !$errors):
$rec = $records[0];
?>
<div class="single-label-wrap">
<div class="single-label-feed">
<div class="single-label">
<div class="label-text">
<div class="label-text-line"><?= nl2br(safe($rec['data1'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data2'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data3'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data4'] ?? '')) ?></div>
</div>
<div class="barcode-block">
<?= render_datamatrix_svg((string)($rec['barcode'] ?? '')) ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
</body>
</html>

651
index.php Normal file → Executable file
View File

@@ -1,212 +1,611 @@
<?php
// Databrae (working name)
// Proof of concept
// index.php — Spine/Item Label Generator with CSV + Sheet Template (Demco 1.5"x1.0")
// Prints to exact physical size using CSS inches and @page rules.
// Preset defaults can be adjusted from the UI without code changes.
declare(strict_types=1);
$autoloadOk = file_exists(__DIR__ . '/vendor/autoload.php');
if ($autoloadOk) {
require __DIR__ . '/vendor/autoload.php';
}
// Using Tecnick's barcode library to generate data matrix barcodes
use Com\Tecnick\Barcode\Barcode;
function safe($s) {
return htmlspecialchars(trim($s ?? ''), ENT_QUOTES, 'UTF-8');
function safe($s) { return htmlspecialchars(trim((string)($s ?? '')), ENT_QUOTES, 'UTF-8'); }
function strip_bom(string $s): string {
if (substr($s, 0, 3) === "\xEF\xBB\xBF") return substr($s, 3);
return $s;
}
function detect_delimiter(string $headerLine): string {
$candidates = [",", "\t", ";", "|"];
$counts = [];
foreach ($candidates as $d) $counts[$d] = substr_count($headerLine, $d);
arsort($counts);
return array_key_first($counts) ?: ",";
}
function read_csv_assoc(string $tmpPath, array &$errors): array {
$rows = [];
$required = ['data1','data2','data3','data4','barcode'];
$raw = file_get_contents($tmpPath);
if ($raw === false || $raw === '') { $errors[] = "Uploaded CSV is empty or unreadable."; return []; }
$raw = strip_bom($raw);
$lines = preg_split('/\r\n|\r|\n/', $raw);
if (!$lines || count($lines) < 1) { $errors[] = "CSV appears to have no lines."; return []; }
$headerLine = '';
foreach ($lines as $ln) { if (trim($ln) !== '') { $headerLine = $ln; break; } }
if ($headerLine === '') { $errors[] = "CSV header line is missing."; return []; }
$delimiter = detect_delimiter($headerLine);
$fh = fopen($tmpPath, 'r'); if (!$fh) { $errors[] = "Unable to open uploaded CSV."; return []; }
// Read header
$hdr = [];
while (($line = fgets($fh)) !== false) {
$line = strip_bom($line);
if (trim($line) === '') continue;
$hdr = str_getcsv($line, $delimiter);
break;
}
if (!$hdr) { $errors[] = "Could not read header row."; fclose($fh); return []; }
$headers = array_map(fn($h) => strtolower(trim((string)$h)), $hdr);
foreach (['data1','data2','data3','data4','barcode'] as $req) {
if (!in_array($req, $headers, true)) $errors[] = "Missing required header: <code>{$req}</code>.";
}
if ($errors) { fclose($fh); return []; }
while (($row = fgetcsv($fh, 0, $delimiter)) !== false) {
if ($row === [null] || $row === false) continue;
if (count(array_filter($row, fn($v) => trim((string)$v) !== '')) === 0) continue; // skip blank line
$assoc = [];
foreach ($required as $key) {
$idx = array_search($key, $headers, true);
$assoc[$key] = $idx !== false && isset($row[$idx]) ? trim((string)$row[$idx]) : '';
}
if (implode('', $assoc) === '') continue;
$rows[] = $assoc;
}
fclose($fh);
if (empty($rows)) $errors[] = "CSV contained no usable rows after the header.";
return $rows;
}
function render_datamatrix_svg(string $content): string {
global $autoloadOk;
if (!$autoloadOk) return '<!-- Barcode library not installed -->';
$barcode = new Barcode();
$obj = $barcode->getBarcodeObj(
'DATAMATRIX',
$content,
-1, -1, 'black', [0,0,0,0]
);
$obj->setBackgroundColor('white');
return $obj->getSvgCode();
}
// Set up the form input
/* ---------------- Sheet Template System ---------------- */
// Default preset: Demco 1.5" x 1.0" on Letter (guess: 5x8 grid, 0.5" margins, 0.125" gaps).
// You can adjust these in the UI and re-preview until alignment is perfect.
$defaults = [
'page_width_in' => 8.5,
'page_height_in' => 11.0,
'margin_top_in' => 0.5,
'margin_right_in' => 0.5,
'margin_bottom_in' => 0.5,
'margin_left_in' => 0.5,
'cols' => 5,
'rows' => 8,
'label_w_in' => 1.5,
'label_h_in' => 1.0,
'gap_h_in' => 0.125, // horizontal gap between columns
'gap_v_in' => 0.125, // vertical gap between rows
'barcode_h_in' => 0.30, // barcode height inside 1.0" label
'font_in' => 0.12, // approx 8.6pt
'show_outlines' => '1'
];
// Pull current template params from POST (template form) or keep defaults
function valf($name, $def) {
return isset($_POST[$name]) && $_POST[$name] !== '' ? $_POST[$name] : $def;
}
$page_width_in = (float) valf('page_width_in', $defaults['page_width_in']);
$page_height_in = (float) valf('page_height_in', $defaults['page_height_in']);
$margin_top_in = (float) valf('margin_top_in', $defaults['margin_top_in']);
$margin_right_in = (float) valf('margin_right_in', $defaults['margin_right_in']);
$margin_bottom_in = (float) valf('margin_bottom_in', $defaults['margin_bottom_in']);
$margin_left_in = (float) valf('margin_left_in', $defaults['margin_left_in']);
$cols = max(1, (int) valf('cols', $defaults['cols']));
$rows = max(1, (int) valf('rows', $defaults['rows']));
$label_w_in = (float) valf('label_w_in', $defaults['label_w_in']);
$label_h_in = (float) valf('label_h_in', $defaults['label_h_in']);
$gap_h_in = (float) valf('gap_h_in', $defaults['gap_h_in']);
$gap_v_in = (float) valf('gap_v_in', $defaults['gap_v_in']);
$barcode_h_in = (float) valf('barcode_h_in', $defaults['barcode_h_in']);
$font_in = (float) valf('font_in', $defaults['font_in']);
$show_outlines = isset($_POST['show_outlines']) ? '1' : $defaults['show_outlines'];
// Records pipeline (single or CSV)
$errors = [];
$records = [];
$isPosted = ($_SERVER['REQUEST_METHOD'] === 'POST');
$data1 = $_POST['data1'] ?? '';
$data2 = $_POST['data2'] ?? '';
$data3 = $_POST['data3'] ?? '';
$data4 = $_POST['data4'] ?? '';
$barcodeText = $_POST['barcode'] ?? '';
$isPosted = ($_SERVER['REQUEST_METHOD'] === 'POST');
// Declare some variables
$svgBarcode = '';
$errorMsg = '';
$mode = $_POST['mode'] ?? '';
// Generate the data matrix barcode and handle some common errors where I've already screwed up
if ($isPosted) {
if (!$autoloadOk) {
$errorMsg = 'Composer autoload not found. Run <code>composer require tecnickcom/tc-lib-barcode</code> in this folder.';
} elseif ($barcodeText === '') {
$errorMsg = 'Please provide a Barcode value.';
if ($isPosted && $mode === 'csv') {
if (!isset($_FILES['csvfile']) || !is_uploaded_file($_FILES['csvfile']['tmp_name'])) {
$errors[] = "Please choose a CSV file to upload.";
} else {
try {
$barcode = new Barcode();
// Create the data matrix as an SVG
$bobj = $barcode->getBarcodeObj(
'DATAMATRIX',
$barcodeText,
-1, // width auto
-1, // height auto
'black',
[0, 0, 0, 0]
);
$bobj->setBackgroundColor('white');
$svgBarcode = $bobj->getSvgCode();
} catch (Throwable $e) {
$errorMsg = 'Barcode generation error: ' . safe($e->getMessage());
}
$size = $_FILES['csvfile']['size'] ?? 0;
if ($size > 10 * 1024 * 1024) { $errors[] = "CSV too large (max 10MB)."; }
else { $records = read_csv_assoc($_FILES['csvfile']['tmp_name'], $errors); }
}
} elseif ($isPosted && $mode === 'single') {
if ($barcodeText === '') $errors[] = "Please provide a Barcode value.";
else {
$records[] = ['data1'=>$data1,'data2'=>$data2,'data3'=>$data3,'data4'=>$data4,'barcode'=>$barcodeText];
}
}
// Single-label physical dimensions (as fed by the label printer: 3.7cm wide × 2.5cm tall)
// Content is rotated 90° so it reads correctly when applied to a spine.
$single_feed_w_cm = 3.7;
$single_feed_h_cm = 2.5;
// After rotation, the visible label area is 2.5cm wide × 3.7cm tall
$single_label_w_cm = 2.5;
$single_label_h_cm = 3.7;
$is_single = ($mode === 'single' && !empty($records));
// Compute pagination (CSV/sheet mode only)
$perPage = $cols * $rows;
$pages = [];
if ($records && !$is_single) {
$pages = array_chunk($records, $perPage);
}
?>
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Spine Label Generator</title>
<title>Label Generator — CSV + Sheet Template</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
:root {
--font: "Helvetica Neue", Arial, sans-serif;
/* These CSS variables are set from PHP inline below to match your template */
--page-w: <?= $page_width_in ?>in;
--page-h: <?= $page_height_in ?>in;
--m-top: <?= $margin_top_in ?>in;
--m-right: <?= $margin_right_in ?>in;
--m-bottom: <?= $margin_bottom_in ?>in;
--m-left: <?= $margin_left_in ?>in;
--cols: <?= $cols ?>;
--rows: <?= $rows ?>;
--label-w: <?= $label_w_in ?>in;
--label-h: <?= $label_h_in ?>in;
--gap-h: <?= $gap_h_in ?>in;
--gap-v: <?= $gap_v_in ?>in;
--barcode-h: <?= $barcode_h_in ?>in;
--font-in: <?= $font_in ?>in;
--show-outlines: <?= $show_outlines === '1' ? 1 : 0 ?>;
}
<?php if ($is_single): ?>
@page {
size: <?= $single_feed_w_cm ?>cm <?= $single_feed_h_cm ?>cm landscape;
margin: 0;
}
<?php else: ?>
@page {
size: var(--page-w) var(--page-h);
margin: 0; /* we handle margins inside the .sheet via padding */
}
<?php endif; ?>
body {
font-family: var(--font);
margin: 1.5rem;
color: #222;
line-height: 1.35;
}
form {
h1 { margin-bottom: .25rem; }
.subtle { color: #666; margin-top: 0; }
.wrap {
display: grid;
gap: .75rem;
max-width: 520px;
margin-bottom: 1.5rem;
gap: 1.25rem;
max-width: 1200px;
}
.pane {
border: 1px solid #ddd;
border-radius: 10px;
padding: 1rem;
}
.pane h2 { margin: 0 0 .75rem; font-size: 1.1rem; }
form { display: grid; gap: .75rem; }
.grid2 {
display: grid;
grid-template-columns: 1fr 1fr;
gap: .75rem 1rem;
}
label { font-weight: 600; }
input[type="text"] {
input[type="text"], input[type="number"], input[type="file"] {
padding: .5rem .6rem;
border: 1px solid #bbb;
border-radius: 6px;
font-size: 1rem;
width: 100%;
background: #fff;
}
.row { display: grid; gap: .35rem; }
.actions { margin-top: .5rem; }
.actions { display: flex; gap: .5rem; flex-wrap: wrap; }
button {
padding: .6rem .9rem;
border: 1px solid #444;
background: #111;
color: #fff;
border-radius: 8px;
cursor: pointer;
font-weight: 600;
}
button:hover { filter: brightness(1.05); }
.note {
font-size: .9rem;
color: #555;
background: #111; color: #fff;
border-radius: 8px; cursor: pointer; font-weight: 600;
}
.ghost { background: #f7f7f7; color: #222; border-color: #ccc; }
.info { font-size: .9rem; color: #555; }
.error {
padding: .75rem 1rem;
border: 1px solid #d33;
background: #fee;
color: #a00;
border-radius: 8px;
max-width: 520px;
margin-bottom: 1rem;
padding: .75rem 1rem; border: 1px solid #d33; background: #fee; color: #a00;
border-radius: 8px; margin-bottom: .75rem;
}
/* ===== Label (exact print size) ===== */
.preview-wrap {
display: grid;
gap: 1rem;
/* ====== SHEET LAYOUT ====== */
.sheets { display: grid; gap: 1rem; }
.sheet {
width: var(--page-w);
height: var(--page-h);
padding: var(--m-top) var(--m-right) var(--m-bottom) var(--m-left);
box-sizing: border-box;
background: white;
position: relative;
page-break-after: always;
}
.label-card {
width: 2.5cm; /* fixed physical width */
height: 3.5cm; /* fixed physical height */
box-sizing: border-box; /* include padding in the fixed size */
border: 1px dashed #aaa;/* guide border (hidden on print) */
padding: 0.2cm; /* compact padding to fit content */
.grid {
width: 100%;
height: 100%;
display: grid;
grid-template-columns: repeat(<?= $cols ?>, var(--label-w));
grid-template-rows: repeat(<?= $rows ?>, var(--label-h));
column-gap: var(--gap-h);
row-gap: var(--gap-v);
justify-content: start; /* no stretching */
align-content: start;
}
.cell {
width: var(--label-w);
height: var(--label-h);
box-sizing: border-box;
padding: 0.06in; /* small inner padding for aesthetics; adjust if needed */
display: flex;
flex-direction: column;
justify-content: space-between; /* push barcode to bottom */
justify-content: space-between;
background: #fff;
border: calc(var(--show-outlines) * 1px) dashed #999; /* outline toggle for test prints */
}
.label-text {
display: grid;
gap: 0.1cm; /* tight vertical spacing between Data 13 */
text-align: center; /* center the three lines */
margin: .2cm;
gap: 0.04in;
text-align: center;
margin: 0;
line-height: 1.2;
font-size: 0.35cm; /* ~8pt—adjust as needed */
font-size: var(--font-in);
word-break: break-word;
}
.label-text-line { white-space: pre-wrap; }
.barcode-block {
.barcode-block { display: flex; justify-content: center; align-items: flex-end; margin-top: 0.02in; }
.barcode-block svg { display: block; height: var(--barcode-h); width: auto; }
.controls { display: flex; gap: .5rem; align-items: center; flex-wrap: wrap; }
/* ====== SINGLE LABEL (standalone) ====== */
.single-label-wrap {
display: flex;
justify-content: center; /* center code horizontally */
align-items: flex-end;
margin-top: 0.05cm; /* tiny breathing room under Data 3 */
justify-content: center;
padding: 1rem 0;
}
/* Keep the Data Matrix exactly 1.5 cm in height; width auto to keep it square */
.barcode-block svg {
.single-label-feed {
/* Outer box matches the physical feed size (3.7cm × 2.5cm landscape) */
width: <?= $single_feed_w_cm ?>cm;
height: <?= $single_feed_h_cm ?>cm;
overflow: hidden;
background: #fff;
border: 1px dashed #999;
position: relative;
}
.single-label {
/* Inner content sized as portrait (2.5cm × 3.7cm), then rotated to fit the landscape feed */
width: <?= $single_label_w_cm ?>cm;
height: <?= $single_label_h_cm ?>cm;
box-sizing: border-box;
padding: 0.2cm;
display: flex;
flex-direction: column;
justify-content: space-between;
font-size: 11pt;
/* Rotate 90° clockwise and re-center within the feed box */
transform: rotate(90deg);
transform-origin: center center;
position: absolute;
top: 50%;
left: 50%;
margin-top: calc(-<?= $single_label_h_cm ?>cm / 2);
margin-left: calc(-<?= $single_label_w_cm ?>cm / 2);
}
.single-label .label-text {
display: grid;
gap: 0.04cm;
text-align: center;
margin: 0;
line-height: 1.2;
font-size: inherit;
word-break: break-word;
}
.single-label .barcode-block {
display: flex;
justify-content: center;
align-items: flex-end;
margin-top: 0.02cm;
}
.single-label .barcode-block svg {
display: block;
height: .5cm; /* requested fixed height */
height: 0.8cm;
width: auto;
}
@media print {
form, .note, .error, h1 { display: none !important; }
body { margin: 0; }
.label-card { border: none; } /* remove guide border on print */
/* Ensure the browser doesn't scale */
@page { margin: 0; }
.pane, .controls, .error, .info, .subtle, h1, .wrap { display: none !important; }
body { margin: 0; padding: 0; background: white; }
.sheet { border: none; }
/* Single label: fill the page exactly */
.single-label-wrap { display: flex !important; padding: 0; margin: 0; justify-content: start; }
.single-label-feed {
width: <?= $single_feed_w_cm ?>cm;
height: <?= $single_feed_h_cm ?>cm;
border: none;
margin: 0;
}
/* Sheet mode: show sheets */
.sheets { display: grid !important; }
}
</style>
<script>
function goPrint(){ window.print(); }
function jump(id){ document.getElementById(id)?.scrollIntoView({behavior:'smooth'}); }
</script>
</head>
<body>
<h1>Spine Label Generator</h1>
<h1>Label Generator</h1>
<p class="subtle">Import CSV and print to a **1.5" × 1.0"** template (Demco) at <strong>100% / Actual size</strong>.</p>
<form method="post">
<div class="row">
<label for="data1">Data 1</label>
<input type="text" id="data1" name="data1" value="<?= safe($data1) ?>" placeholder="e.g., QA76.73.P224">
</div>
<div class="row">
<label for="data2">Data 2</label>
<input type="text" id="data2" name="data2" value="<?= safe($data2) ?>" placeholder="e.g., 2025">
</div>
<div class="row">
<label for="data3">Data 3</label>
<input type="text" id="data3" name="data3" value="<?= safe($data3) ?>" placeholder="e.g., v.2">
</div>
<div class="row">
<label for="data4">Data 4</label>
<input type="text" id="data4" name="data4" value="<?= safe($_POST['data4'] ?? '') ?>" placeholder="e.g., Copy 1">
</div>
<div class="row">
<label for="barcode">Barcode (alphanumeric)</label>
<input type="text" id="barcode" name="barcode" value="<?= safe($barcodeText) ?>" placeholder="e.g., 1230004231819">
</div>
<div class="actions">
<button type="submit">Build Label</button>
</div>
<p class="note">Tip: In the browser print dialog, set scale to <strong>100% / Actual size</strong>.</p>
</form>
<div class="wrap">
<?php if ($errorMsg): ?>
<div class="error"><?= $errorMsg ?></div>
<?php endif; ?>
<!-- Single label form -->
<div class="pane">
<h2>Single Label</h2>
<form method="post">
<input type="hidden" name="mode" value="single">
<!-- Preserve template params across submissions -->
<?php foreach ([
'page_width_in','page_height_in','margin_top_in','margin_right_in','margin_bottom_in','margin_left_in',
'cols','rows','label_w_in','label_h_in','gap_h_in','gap_v_in','barcode_h_in','font_in'
] as $p): ?>
<input type="hidden" name="<?= $p ?>" value="<?= safe($_POST[$p] ?? $defaults[$p] ?? '') ?>">
<?php endforeach; ?>
<?php if ($show_outlines === '1'): ?><input type="hidden" name="show_outlines" value="1"><?php endif; ?>
<?php if ($isPosted && !$errorMsg): ?>
<div class="preview-wrap">
<div class="label-card">
<div class="label-text">
<div class="label-text-line"><?= nl2br(safe($data1)) ?></div>
<div class="label-text-line"><?= nl2br(safe($data2)) ?></div>
<div class="label-text-line"><?= nl2br(safe($data3)) ?></div>
<div class="label-text-line"><?= nl2br(safe($_POST['data4'] ?? '')) ?></div>
<div class="grid2">
<div>
<label for="data1">Data 1</label>
<input type="text" id="data1" name="data1" value="<?= safe($data1) ?>" placeholder="e.g., QA76.73.P224">
</div>
<div>
<label for="data2">Data 2</label>
<input type="text" id="data2" name="data2" value="<?= safe($data2) ?>" placeholder="e.g., 2025">
</div>
<div>
<label for="data3">Data 3</label>
<input type="text" id="data3" name="data3" value="<?= safe($data3) ?>" placeholder="e.g., v.2">
</div>
<div>
<label for="data4">Data 4</label>
<input type="text" id="data4" name="data4" value="<?= safe($data4) ?>" placeholder="e.g., Copy 1">
</div>
<div style="grid-column: 1 / -1;">
<label for="barcode">Barcode (alphanumeric)</label>
<input type="text" id="barcode" name="barcode" value="<?= safe($barcodeText) ?>" placeholder="e.g., 1230004231819">
</div>
</div>
<div class="actions">
<button type="submit">Preview Single Label</button>
<button type="button" class="ghost" onclick="jump('csvpane')">Or Import CSV…</button>
</div>
<p class="info">Data Matrix prints as SVG at <strong><?= number_format($barcode_h_in, 2) ?> in</strong> height.</p>
</form>
</div>
<!-- CSV import -->
<div class="pane" id="csvpane">
<h2>Import CSV (data1,data2,data3,data4,barcode)</h2>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="mode" value="csv">
<!-- persist template params -->
<div class="grid2">
<div style="grid-column: 1 / -1;">
<label for="csvfile">Choose CSV file</label>
<input type="file" id="csvfile" name="csvfile" accept=".csv,text/csv">
</div>
</div>
<?php foreach ([
'page_width_in','page_height_in','margin_top_in','margin_right_in','margin_bottom_in','margin_left_in',
'cols','rows','label_w_in','label_h_in','gap_h_in','gap_v_in','barcode_h_in','font_in'
] as $p): ?>
<input type="hidden" name="<?= $p ?>" value="<?= safe($_POST[$p] ?? $defaults[$p] ?? '') ?>">
<?php endforeach; ?>
<?php if ($show_outlines === '1'): ?><input type="hidden" name="show_outlines" value="1"><?php endif; ?>
<div class="actions">
<button type="submit">Upload & Preview Labels</button>
<button type="reset" class="ghost">Reset</button>
</div>
<p class="info">Delimiters (comma, tab, semicolon) auto-detected. Headers must match exactly.</p>
</form>
</div>
<!-- Template settings -->
<div class="pane">
<h2>Template (Demco 1.5" × 1.0")</h2>
<form method="post">
<div class="grid2">
<div><label>Page width (in)</label><input type="number" step="0.01" name="page_width_in" value="<?= safe($page_width_in) ?>"></div>
<div><label>Page height (in)</label><input type="number" step="0.01" name="page_height_in" value="<?= safe($page_height_in) ?>"></div>
<div><label>Margin top (in)</label><input type="number" step="0.01" name="margin_top_in" value="<?= safe($margin_top_in) ?>"></div>
<div><label>Margin right (in)</label><input type="number" step="0.01" name="margin_right_in" value="<?= safe($margin_right_in) ?>"></div>
<div><label>Margin bottom (in)</label><input type="number" step="0.01" name="margin_bottom_in" value="<?= safe($margin_bottom_in) ?>"></div>
<div><label>Margin left (in)</label><input type="number" step="0.01" name="margin_left_in" value="<?= safe($margin_left_in) ?>"></div>
<div><label>Columns</label><input type="number" step="1" min="1" name="cols" value="<?= safe($cols) ?>"></div>
<div><label>Rows</label><input type="number" step="1" min="1" name="rows" value="<?= safe($rows) ?>"></div>
<div><label>Label width (in)</label><input type="number" step="0.01" name="label_w_in" value="<?= safe($label_w_in) ?>"></div>
<div><label>Label height (in)</label><input type="number" step="0.01" name="label_h_in" value="<?= safe($label_h_in) ?>"></div>
<div><label>Horizontal gap (in)</label><input type="number" step="0.01" name="gap_h_in" value="<?= safe($gap_h_in) ?>"></div>
<div><label>Vertical gap (in)</label><input type="number" step="0.01" name="gap_v_in" value="<?= safe($gap_v_in) ?>"></div>
<div><label>Barcode height (in)</label><input type="number" step="0.01" name="barcode_h_in" value="<?= safe($barcode_h_in) ?>"></div>
<div><label>Font size (in)</label><input type="number" step="0.01" name="font_in" value="<?= safe($font_in) ?>"></div>
<div style="grid-column: 1 / -1; display:flex; align-items:center; gap:.5rem;">
<input type="checkbox" id="show_outlines" name="show_outlines" value="1" <?= $show_outlines==='1'?'checked':'' ?>>
<label for="show_outlines">Show label outlines (for alignment/test prints)</label>
</div>
</div>
<!-- Keep any uploaded/entered data visible on re-submit by passing the last single entry (optional) -->
<input type="hidden" name="mode" value="<?= safe($mode ?: 'single') ?>">
<input type="hidden" name="data1" value="<?= safe($data1) ?>">
<input type="hidden" name="data2" value="<?= safe($data2) ?>">
<input type="hidden" name="data3" value="<?= safe($data3) ?>">
<input type="hidden" name="data4" value="<?= safe($data4) ?>">
<input type="hidden" name="barcode" value="<?= safe($barcodeText) ?>">
<div class="actions">
<button type="submit">Apply Template Settings</button>
<button type="button" class="ghost" onclick="goPrint()">Print</button>
</div>
<p class="info">
<strong>Tip:</strong> Do a test print on plain paper with “Show label outlines” checked.
Hold it behind a real sheet to check alignment, then tweak margins/gaps as needed.
</p>
</form>
</div>
<!-- Errors -->
<?php if (!empty($errors)): ?>
<div class="error">
<strong>Issues:</strong>
<ul><?php foreach ($errors as $e): ?><li><?= $e ?></li><?php endforeach; ?></ul>
</div>
<?php endif; ?>
<!-- Preview / Sheets (CSV mode) -->
<?php if ($records && !$errors && !$is_single): ?>
<div class="controls">
<button onclick="goPrint()">Print</button>
<span class="info"><?= count($records) ?> label(s), <?= count($pages) ?> sheet(s) @ <?= $cols ?>×<?= $rows ?>.</span>
</div>
<div class="barcode-block">
<?= $svgBarcode /* Inline SVG keeps print quality high */ ?>
<div class="sheets">
<?php foreach ($pages as $pageIndex => $pageRecs): ?>
<div class="sheet">
<div class="grid">
<?php
// Fill cells row-major: put records first, blank cells if fewer than capacity.
$count = count($pageRecs);
$capacity = $cols * $rows;
for ($i = 0; $i < $capacity; $i++):
$rec = $pageRecs[$i] ?? null;
?>
<div class="cell">
<?php if ($rec): ?>
<div class="label-text">
<div class="label-text-line"><?= nl2br(safe($rec['data1'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data2'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data3'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data4'] ?? '')) ?></div>
</div>
<div class="barcode-block">
<?= render_datamatrix_svg((string)($rec['barcode'] ?? '')) ?>
</div>
<?php endif; ?>
</div>
<?php endfor; ?>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<!-- Single label preview -->
<?php if ($is_single && !$errors):
$rec = $records[0];
?>
<div class="controls">
<button onclick="goPrint()">Print Label</button>
<span class="info">Single label — <?= $single_label_w_cm ?>cm × <?= $single_label_h_cm ?>cm</span>
</div>
<?php endif; ?>
<?php if (!$autoloadOk): ?>
<div class="error">
Composer autoload not found. Install the barcode library with:<br>
<code>composer require tecnickcom/tc-lib-barcode</code><br>
(Make sure BCMath is enabled.)
</div>
<?php endif; ?>
</div>
<?php if ($is_single && !$errors):
$rec = $records[0];
?>
<div class="single-label-wrap">
<div class="single-label-feed">
<div class="single-label">
<div class="label-text">
<div class="label-text-line"><?= nl2br(safe($rec['data1'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data2'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data3'] ?? '')) ?></div>
<div class="label-text-line"><?= nl2br(safe($rec['data4'] ?? '')) ?></div>
</div>
<div class="barcode-block">
<?= render_datamatrix_svg((string)($rec['barcode'] ?? '')) ?>
</div>
</div>
</div>
</div>