Files
Databrae/index.php

615 lines
25 KiB
PHP
Executable File
Raw Permalink 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.
<?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>Databrae - Library Items and Access</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>Databrae - Label Generator</h1>
<p class="subtle">Create spine labels with tracking barcodes</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>