From 558bd3cec5ff5b52eb8e6a09fad9868b2e347e74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Strausmann?= Date: Sun, 17 May 2026 21:24:41 +0000 Subject: [PATCH] docs(api): pure-vector SVG samples for all 12 seed templates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds backend/app/services/svg_renderer.py which produces SVG strings that mirror the LabelRenderer's pixel coordinate system 1:1. Text elements are pure , QR codes use qrcode's SvgPathImage factory (box_size=1, border=0) with the extracted and scaled via transform="translate(x,y) scale(factor)" — no raster embeds at all. Each SVG has a gray tape outline + 18px annotation strip above the tape showing template key, tape width, and text-line count. CLI script backend/scripts/generate_template_svgs.py iterates all seed templates that have preview_sample and writes one SVG per template to docs/site/operations/templates/svg-samples/. Makefile target docs-svg-samples wraps the script. Docs page docs/site/operations/templates/layouts.md embeds all 12 SVGs via tags grouped by tape width for side-by-side comparison. 11 new unit tests in test_svg_renderer.py cover: valid XML, no raster embeds, correct viewBox per tape_mm, tape outline rect, QR as , text value rendering, annotation strip, list join with ' | ', XML escaping, unsupported tape_mm error, and distinct heights per tape. These SVGs are the visual basis for Phase 7e #81 (semantic qr-first + N-text-lines layout system v2). Refs #22 --- Makefile | 6 + backend/app/services/svg_renderer.py | 205 ++++++++++++++++++ backend/scripts/generate_template_svgs.py | 67 ++++++ .../tests/unit/services/test_svg_renderer.py | 191 ++++++++++++++++ docs/site/operations/templates/layouts.md | 127 +++++++++++ .../templates/svg-samples/grocy-12mm.svg | 7 + .../templates/svg-samples/grocy-18mm.svg | 8 + .../templates/svg-samples/grocy-24mm.svg | 8 + .../templates/svg-samples/qr-only-12mm.svg | 5 + .../templates/svg-samples/qr-only-18mm.svg | 5 + .../templates/svg-samples/qr-only-24mm.svg | 5 + .../templates/svg-samples/snipeit-12mm.svg | 7 + .../templates/svg-samples/snipeit-18mm.svg | 8 + .../templates/svg-samples/snipeit-24mm.svg | 8 + .../templates/svg-samples/spoolman-12mm.svg | 7 + .../templates/svg-samples/spoolman-18mm.svg | 8 + .../templates/svg-samples/spoolman-24mm.svg | 8 + 17 files changed, 680 insertions(+) create mode 100644 Makefile create mode 100644 backend/app/services/svg_renderer.py create mode 100644 backend/scripts/generate_template_svgs.py create mode 100644 backend/tests/unit/services/test_svg_renderer.py create mode 100644 docs/site/operations/templates/layouts.md create mode 100644 docs/site/operations/templates/svg-samples/grocy-12mm.svg create mode 100644 docs/site/operations/templates/svg-samples/grocy-18mm.svg create mode 100644 docs/site/operations/templates/svg-samples/grocy-24mm.svg create mode 100644 docs/site/operations/templates/svg-samples/qr-only-12mm.svg create mode 100644 docs/site/operations/templates/svg-samples/qr-only-18mm.svg create mode 100644 docs/site/operations/templates/svg-samples/qr-only-24mm.svg create mode 100644 docs/site/operations/templates/svg-samples/snipeit-12mm.svg create mode 100644 docs/site/operations/templates/svg-samples/snipeit-18mm.svg create mode 100644 docs/site/operations/templates/svg-samples/snipeit-24mm.svg create mode 100644 docs/site/operations/templates/svg-samples/spoolman-12mm.svg create mode 100644 docs/site/operations/templates/svg-samples/spoolman-18mm.svg create mode 100644 docs/site/operations/templates/svg-samples/spoolman-24mm.svg diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..04aa113 --- /dev/null +++ b/Makefile @@ -0,0 +1,6 @@ +.PHONY: docs-svg-samples + +## docs-svg-samples — regenerate pure-vector SVG previews for all seed templates +## Output: docs/site/operations/templates/svg-samples/{template-id}.svg +docs-svg-samples: + cd backend && uv run python scripts/generate_template_svgs.py diff --git a/backend/app/services/svg_renderer.py b/backend/app/services/svg_renderer.py new file mode 100644 index 0000000..81420e9 --- /dev/null +++ b/backend/app/services/svg_renderer.py @@ -0,0 +1,205 @@ +"""Render a label template as a pure-vector SVG string. + +The SVG mirrors the LabelRenderer's pixel coordinate system 1:1 (top-left +origin, 300 DPI Brother geometry) so that SVG previews match what gets printed. + +QR codes are rendered as inline pure-vector ```` elements using +qrcode's SvgPathImage factory. All text elements become ```` nodes — +no raster embeds for text. A gray ```` outlines the tape boundary. + +Coordinate system: + - The tape's printable height is taken from TAPE_HEIGHT_PX, same as + LabelRenderer. + - The canvas width is fixed at DEFAULT_LABEL_WIDTH_PX (600 px), same as + LabelRenderer, so element x/y coordinates translate 1:1. + - An annotation strip of ANNOTATION_HEIGHT_PX is reserved above the tape + rect so the viewBox shows the title line outside the printable area. +""" + +from __future__ import annotations + +import re +import xml.etree.ElementTree as ET + +import qrcode +import qrcode.image.svg + +from app.services.label_renderer import DEFAULT_LABEL_WIDTH_PX, TAPE_HEIGHT_PX + +# Extra vertical space above the tape rect used for the template-key annotation. +_ANNOTATION_HEIGHT_PX: int = 18 + + +def _qr_svg_group(qr_data: str, x: int, y: int, size_px: int) -> str: + """Return an SVG ```` element containing the QR code as a pure-vector path. + + The path is extracted from qrcode's SvgPathImage output and scaled so that + the QR module fills exactly ``size_px x size_px`` pixels at the given (x, y) + position. + + Args: + qr_data: The payload to encode. + x: Left edge in the tape's pixel coordinate system. + y: Top edge in the tape's pixel coordinate system. + size_px: Target width/height in pixels. + + Returns: + A ``...`` string ready for embedding in the SVG. + """ + # Use box_size=1 + border=0 so the path coordinates are in module units + # (integers), making the scale calculation straightforward. + factory = qrcode.image.svg.SvgPathImage + qr_img = qrcode.make( + qr_data, + image_factory=factory, + box_size=1, + border=0, + ) + raw_svg = qr_img.to_string(encoding="unicode") + + # Parse the outer to grab the viewBox dimensions and the element. + root = ET.fromstring(raw_svg) + ns = {"svg": "http://www.w3.org/2000/svg"} + path_el = root.find("svg:path", ns) + if path_el is None: + # Fallback: try without namespace (some qrcode versions omit it) + path_el = root.find("path") + if path_el is None: + raise RuntimeError(f"qrcode SvgPathImage produced no element for data={qr_data!r}") + + path_d = path_el.attrib.get("d", "") + + # Derive the QR grid size from the viewBox. With box_size=1 the viewBox + # width equals the number of modules. + vb = root.attrib.get("viewBox", "") + vb_parts = vb.split() + if len(vb_parts) == 4: + qr_units = float(vb_parts[2]) # width in module units + else: + # Parse from width attribute ("29mm" etc.) as fallback. + w_str = root.attrib.get("width", "1") + qr_units = float(re.sub(r"[^0-9.]", "", w_str) or "1") + + scale = size_px / qr_units if qr_units else 1.0 + + # Shift the annotation offset: QR y is in tape-space so we add + # the annotation strip below in the outer SVG, not here. + return ( + f'' + f'' + f"" + ) + + +def _annotation_label(template_id: str, tape_mm: int, elements: list[dict[str, object]]) -> str: + """Return a short human-readable title string for the SVG annotation strip.""" + text_count = sum(1 for el in elements if el.get("type") == "text") + plural = "s" if text_count != 1 else "" + return f"{template_id} — tape {tape_mm}mm — {text_count} text line{plural}" + + +def _resolve_sample_value(field: str, sample: dict[str, object]) -> str: + """Look up *field* in *sample*, joining list values with ' | '.""" + value = sample.get(field, "") + if isinstance(value, (list, tuple)): + return " | ".join(str(v) for v in value) + return str(value) + + +def render_template_svg( + template_definition: dict[str, object], sample_data: dict[str, object] +) -> str: + """Render a template's preview as a pure-vector SVG string. + + The SVG mirrors the LabelRenderer's pixel coordinate system 1:1 so it + matches the print output. QR codes are rendered as inline ```` + using python-qrcode's SvgPathImage factory; text elements become + ````; the tape outline is a ```` with a 1px gray border. + + Args: + template_definition: contents of Template.definition JSON column + (already deserialised). Must include ``tape_mm`` and ``elements``. + sample_data: per-template preview_sample dict (already validated). + + Returns: + Full SVG XML as a string starting with ````. + """ + tape_mm = int(str(template_definition["tape_mm"])) + tape_h = TAPE_HEIGHT_PX.get(tape_mm) + if tape_h is None: + raise ValueError(f"Unsupported tape_mm: {tape_mm}. Supported: {sorted(TAPE_HEIGHT_PX)}") + + w = DEFAULT_LABEL_WIDTH_PX + raw_elements = template_definition.get("elements", []) + element_list: list[object] = list(raw_elements) if isinstance(raw_elements, list) else [] + elements: list[dict[str, object]] = [dict(el) for el in element_list if isinstance(el, dict)] + template_id = str(template_definition.get("id", "unknown")) + + # Total SVG height = annotation strip + tape body + total_h = _ANNOTATION_HEIGHT_PX + tape_h + + # viewBox: origin is at the top-left of the annotation strip; the tape + # rect starts at y=_ANNOTATION_HEIGHT_PX. + vb = f"0 0 {w} {total_h}" + + lines: list[str] = [] + lines.append( + f'' + ) + + # — Annotation strip (outside the printable tape area) —————————————— + annotation = _annotation_label(template_id, tape_mm, elements) + lines.append( + f' ' + f"{annotation}" + f"" + ) + + # — Tape background + outline —————————————————————————————————————— + ty = _ANNOTATION_HEIGHT_PX # tape top y in SVG coordinates + lines.append( + f' ' + ) + + # — Label elements ————————————————————————————————————————————————— + for el in elements: + el_type = str(el.get("type", "")) + ex = int(str(el.get("x", 0))) + ey = int(str(el.get("y", 0))) + + # Shift element y coordinates by the annotation strip height so that + # the element positions in the SVG match the pixel coordinates used + # by the LabelRenderer on the tape. + svg_y = ty + ey + + if el_type == "qr": + data_field = str(el.get("data_field", "qr_payload")) + size_px = int(str(el.get("size", 80))) + qr_data = _resolve_sample_value(data_field, sample_data) + # QR group: translate to tape-offset-adjusted position. + qr_group = _qr_svg_group(qr_data, ex, svg_y, size_px) + lines.append(f" {qr_group}") + + elif el_type == "text": + field = str(el.get("field", "")) + font_size = int(str(el.get("font_size", 14))) + text_value = _resolve_sample_value(field, sample_data) + # Escape XML special characters in user data. + text_value = ( + text_value.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + ) + lines.append( + f' {text_value}' + ) + + lines.append("") + return "\n".join(lines) diff --git a/backend/scripts/generate_template_svgs.py b/backend/scripts/generate_template_svgs.py new file mode 100644 index 0000000..359a01b --- /dev/null +++ b/backend/scripts/generate_template_svgs.py @@ -0,0 +1,67 @@ +"""Generate SVG samples for every seed template under docs/. + +Run with: + cd backend && uv run python scripts/generate_template_svgs.py + +Writes one ``{template-id}.svg`` per seed template that contains a +``preview_sample`` block into: + docs/site/operations/templates/svg-samples/ + +These SVGs are the visual basis for the Phase 7e layout-system brainstorming +(GitHub issue #81). They are pure-vector: text is rendered as ```` +elements, QR codes as ```` elements — no raster embeds. +""" + +from __future__ import annotations + +import sys +from pathlib import Path + +import yaml + +# Ensure the project root is on sys.path so ``app.*`` imports work when the +# script is executed directly via ``uv run python scripts/…`` from the +# ``backend/`` directory. +_BACKEND_DIR = Path(__file__).resolve().parents[1] +if str(_BACKEND_DIR) not in sys.path: + sys.path.insert(0, str(_BACKEND_DIR)) + +from app.services.svg_renderer import render_template_svg # noqa: E402 + +SEED_DIR = _BACKEND_DIR / "app" / "seed" / "templates" +OUT_DIR = _BACKEND_DIR.parent / "docs" / "site" / "operations" / "templates" / "svg-samples" + + +def main() -> int: + """Generate one SVG per seed template that has a preview_sample. + + Returns: + 0 on success, 1 if any template lacks a preview_sample (counted as a + warning, not a failure). + """ + OUT_DIR.mkdir(parents=True, exist_ok=True) + written: list[Path] = [] + skipped: list[str] = [] + + for yaml_file in sorted(SEED_DIR.glob("*.yaml")): + definition: dict[str, object] = yaml.safe_load(yaml_file.read_text()) + sample = definition.get("preview_sample") + if not sample: + print(f"SKIP {yaml_file.name}: no preview_sample") # noqa: T201 + skipped.append(yaml_file.name) + continue + + template_id = str(definition.get("id", yaml_file.stem)) + svg = render_template_svg(definition, dict(sample)) # type: ignore[arg-type] + out_path = OUT_DIR / f"{template_id}.svg" + out_path.write_text(svg, encoding="utf-8") + written.append(out_path) + print(f"WROTE {out_path}") # noqa: T201 + + print() # noqa: T201 + print(f"Done: {len(written)} SVG(s) written, {len(skipped)} skipped.") # noqa: T201 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/backend/tests/unit/services/test_svg_renderer.py b/backend/tests/unit/services/test_svg_renderer.py new file mode 100644 index 0000000..da359e1 --- /dev/null +++ b/backend/tests/unit/services/test_svg_renderer.py @@ -0,0 +1,191 @@ +"""Unit tests for app.services.svg_renderer. + +Verifies that the SVG renderer produces valid XML with correct structure, +pure-vector ```` elements, a QR ```` element, a viewBox that +matches the tape dimensions, and a title annotation strip above the tape +outline. +""" + +from __future__ import annotations + +import xml.etree.ElementTree as ET + +import pytest +from app.services.label_renderer import TAPE_HEIGHT_PX +from app.services.svg_renderer import ( + _ANNOTATION_HEIGHT_PX, + render_template_svg, +) + +SVG_NS = "http://www.w3.org/2000/svg" + + +def _findall(root: ET.Element, local_tag: str) -> list[ET.Element]: + """Find all descendant elements matching a local tag name (ignoring namespace).""" + return root.findall(f".//{{{SVG_NS}}}{local_tag}") + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + + +def _minimal_definition( + tape_mm: int = 12, + *, + include_qr: bool = True, + include_text: bool = True, +) -> dict[str, object]: + elements: list[dict[str, object]] = [] + if include_qr: + elements.append({"type": "qr", "x": 8, "y": 13, "size": 60, "data_field": "qr_payload"}) + if include_text: + elements.append({"type": "text", "x": 80, "y": 18, "font_size": 20, "field": "primary_id"}) + return { + "id": f"test-{tape_mm}mm", + "tape_mm": tape_mm, + "elements": elements, + } + + +def _sample() -> dict[str, object]: + return {"primary_id": "TestValue", "qr_payload": "https://example.com/"} + + +# --------------------------------------------------------------------------- +# Core contract +# --------------------------------------------------------------------------- + + +def test_svg_renderer_produces_valid_xml_with_text_elements() -> None: + definition = _minimal_definition() + svg = render_template_svg(definition, _sample()) + + assert "" in svg + assert "TestValue" in svg # text element rendered as pure + assert "viewBox" in svg + + # Must parse as valid XML without exceptions. + root = ET.fromstring(svg) + assert root.tag == f"{{{SVG_NS}}}svg" + + +def test_svg_contains_no_base64_image_embeds() -> None: + """Text and QR must be pure-vector — no raster embeds allowed.""" + definition = _minimal_definition() + svg = render_template_svg(definition, _sample()) + + assert "data:image/png;base64" not in svg + assert "data:image/jpeg;base64" not in svg + + +def test_svg_viewbox_matches_tape_dimensions() -> None: + """viewBox width=600, height=TAPE_HEIGHT_PX[tape_mm] + annotation strip.""" + for tape_mm in (12, 18, 24): + definition = _minimal_definition(tape_mm) + svg = render_template_svg(definition, _sample()) + root = ET.fromstring(svg) + + expected_h = TAPE_HEIGHT_PX[tape_mm] + _ANNOTATION_HEIGHT_PX + vb = root.attrib.get("viewBox", "") + parts = vb.split() + assert len(parts) == 4, f"Unexpected viewBox for tape_mm={tape_mm}: {vb!r}" + assert int(parts[2]) == 600, f"viewBox width should be 600 for tape_mm={tape_mm}" + assert int(parts[3]) == expected_h, ( + f"viewBox height should be {expected_h} for tape_mm={tape_mm}, got {parts[3]}" + ) + + +def test_svg_contains_tape_outline_rect() -> None: + """A gray must mark the printable tape area.""" + definition = _minimal_definition() + svg = render_template_svg(definition, _sample()) + root = ET.fromstring(svg) + + rects = _findall(root, "rect") + assert rects, "No element found — tape outline is missing" + # The rect should have a gray stroke. + strokes = [el.attrib.get("stroke", "") for el in rects] + assert any(s == "#aaa" for s in strokes), f"Expected gray stroke #aaa on rect, got {strokes}" + + +def test_svg_contains_qr_path_element() -> None: + """QR codes must be rendered as a pure-vector , not .""" + definition = _minimal_definition(include_text=False) + svg = render_template_svg(definition, _sample()) + root = ET.fromstring(svg) + + paths = _findall(root, "path") + assert paths, "No element found — QR code is not rendered as vector" + + images = _findall(root, "image") + assert not images, f"Found unexpected elements: {images}" + + +def test_svg_text_value_is_present() -> None: + """The sample field value must appear verbatim in a element.""" + definition = _minimal_definition(include_qr=False) + svg = render_template_svg(definition, _sample()) + root = ET.fromstring(svg) + + texts = _findall(root, "text") + # Filter out the annotation strip (which has fill="#666") — look for user data. + user_texts = [el for el in texts if el.attrib.get("fill") == "black"] + values = [el.text or "" for el in user_texts] + assert any("TestValue" in v for v in values), ( + f"Expected 'TestValue' in a element, got: {values}" + ) + + +def test_svg_annotation_strip_shows_template_key() -> None: + """The annotation strip above the tape must show the template id.""" + definition = _minimal_definition() + svg = render_template_svg(definition, _sample()) + + assert "test-12mm" in svg # the template id from _minimal_definition + + +def test_svg_list_secondary_field_joined_with_pipe() -> None: + """List values (e.g. 'secondary') must be joined with ' | '.""" + definition: dict[str, object] = { + "id": "test-list", + "tape_mm": 18, + "elements": [{"type": "text", "x": 10, "y": 20, "font_size": 14, "field": "secondary"}], + } + sample: dict[str, object] = {"secondary": ["Alpha", "Beta"], "qr_payload": "x"} + svg = render_template_svg(definition, sample) + assert "Alpha | Beta" in svg + + +def test_svg_xml_special_chars_escaped() -> None: + """Ampersands and angle brackets in sample data must be XML-escaped.""" + definition: dict[str, object] = { + "id": "test-escape", + "tape_mm": 12, + "elements": [{"type": "text", "x": 10, "y": 20, "font_size": 14, "field": "title"}], + } + sample: dict[str, object] = {"title": "A & B < C > D", "qr_payload": "x"} + svg = render_template_svg(definition, sample) + + # The raw ampersand/angle must NOT appear outside CDATA. + # The escaped forms must appear. + assert "A & B < C > D" in svg + # Must still parse as valid XML. + ET.fromstring(svg) + + +def test_svg_unsupported_tape_mm_raises() -> None: + with pytest.raises(ValueError, match="Unsupported tape_mm"): + render_template_svg({"id": "bad", "tape_mm": 99, "elements": []}, {}) + + +def test_svg_all_tape_sizes_produce_different_heights() -> None: + """12mm, 18mm and 24mm tapes must result in different SVG heights.""" + heights = set() + for tape_mm in (12, 18, 24): + definition = _minimal_definition(tape_mm) + svg = render_template_svg(definition, _sample()) + root = ET.fromstring(svg) + heights.add(int(root.attrib.get("height", 0))) + assert len(heights) == 3, f"Expected 3 distinct heights, got: {heights}" diff --git a/docs/site/operations/templates/layouts.md b/docs/site/operations/templates/layouts.md new file mode 100644 index 0000000..6003d4f --- /dev/null +++ b/docs/site/operations/templates/layouts.md @@ -0,0 +1,127 @@ +# Label Template Layouts + +Diese Seite zeigt die aktuellen Phase-3.5/4-Templates als Vektor-SVG. Sie ist die +Diskussions-Grundlage für [Phase 7e #81](https://github.com/strausmann/label-printer-hub/issues/81) +(Template Layout System v2). + +Erzeugt durch `make docs-svg-samples`. + +Die SVGs sind pure-vector: Text ist als ``, QR-Codes als `` gerendert — keine +Raster-Embeds. Das macht Git-Diffs der SVGs sinnvoll wenn sich Layout-Parameter ändern. + +--- + +## 12mm Tape (TAPE_HEIGHT_PX = 106 px) + +### grocy-12mm + + + grocy-12mm label preview + + +### qr-only-12mm + + + qr-only-12mm label preview + + +### snipeit-12mm + + + snipeit-12mm label preview + + +### spoolman-12mm + + + spoolman-12mm label preview + + +--- + +## 18mm Tape (TAPE_HEIGHT_PX = 165 px) + +### grocy-18mm + + + grocy-18mm label preview + + +### qr-only-18mm + + + qr-only-18mm label preview + + +### snipeit-18mm + + + snipeit-18mm label preview + + +### spoolman-18mm + + + spoolman-18mm label preview + + +--- + +## 24mm Tape (TAPE_HEIGHT_PX = 256 px) + +### grocy-24mm + + + grocy-24mm label preview + + +### qr-only-24mm + + + qr-only-24mm label preview + + +### snipeit-24mm + + + snipeit-24mm label preview + + +### spoolman-24mm + + + spoolman-24mm label preview + + +--- + +## Technische Details + +### Koordinatensystem + +Die SVGs spiegeln das Pixel-Koordinatensystem des `LabelRenderer` 1:1 wider: + +| Tape | `TAPE_HEIGHT_PX` | SVG total height | +|------|-----------------|-----------------| +| 12mm | 106 px | 124 px (+ 18 px Annotation) | +| 18mm | 165 px | 183 px (+ 18 px Annotation) | +| 24mm | 256 px | 274 px (+ 18 px Annotation) | + +Canvas-Breite ist immer 600 px (`DEFAULT_LABEL_WIDTH_PX`). Element-Koordinaten (`x`, `y`) +entsprechen direkt den Werten in den YAML-Template-Definitionen. + +### QR-Code-Rendering + +QR-Codes werden über `qrcode.image.svg.SvgPathImage` (box_size=1, border=0) als +``-Element gerendert und via `transform="translate(x,y) scale(factor)"` auf die +Zielgrösse skaliert. Kein Raster-Fallback — der QR-Pfad ist immer pure-vector. + +### Regenerierung + +```bash +make docs-svg-samples +``` + +Die SVGs werden neu erzeugt wenn sich `preview_sample`-Daten oder Element-Koordinaten +in den Seed-Templates ändern. Die generierten Dateien sind versioniert damit sie ohne +Python-Umgebung lesbar sind. diff --git a/docs/site/operations/templates/svg-samples/grocy-12mm.svg b/docs/site/operations/templates/svg-samples/grocy-12mm.svg new file mode 100644 index 0000000..3d6f6f4 --- /dev/null +++ b/docs/site/operations/templates/svg-samples/grocy-12mm.svg @@ -0,0 +1,7 @@ + + grocy-12mm — tape 12mm — 2 text lines + + + Erdbeermarmelade + Lager > Vorrat + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/grocy-18mm.svg b/docs/site/operations/templates/svg-samples/grocy-18mm.svg new file mode 100644 index 0000000..d7ca2d4 --- /dev/null +++ b/docs/site/operations/templates/svg-samples/grocy-18mm.svg @@ -0,0 +1,8 @@ + + grocy-18mm — tape 18mm — 3 text lines + + + Erdbeermarmelade + Lager > Vorrat + MHD 2027-04-30 | 3 Glaeser + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/grocy-24mm.svg b/docs/site/operations/templates/svg-samples/grocy-24mm.svg new file mode 100644 index 0000000..7fd1080 --- /dev/null +++ b/docs/site/operations/templates/svg-samples/grocy-24mm.svg @@ -0,0 +1,8 @@ + + grocy-24mm — tape 24mm — 3 text lines + + + Erdbeermarmelade + Lager > Vorrat + MHD 2027-04-30 | 3 Glaeser + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/qr-only-12mm.svg b/docs/site/operations/templates/svg-samples/qr-only-12mm.svg new file mode 100644 index 0000000..96c4d6b --- /dev/null +++ b/docs/site/operations/templates/svg-samples/qr-only-12mm.svg @@ -0,0 +1,5 @@ + + qr-only-12mm — tape 12mm — 0 text lines + + + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/qr-only-18mm.svg b/docs/site/operations/templates/svg-samples/qr-only-18mm.svg new file mode 100644 index 0000000..85aef7a --- /dev/null +++ b/docs/site/operations/templates/svg-samples/qr-only-18mm.svg @@ -0,0 +1,5 @@ + + qr-only-18mm — tape 18mm — 0 text lines + + + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/qr-only-24mm.svg b/docs/site/operations/templates/svg-samples/qr-only-24mm.svg new file mode 100644 index 0000000..7b51f96 --- /dev/null +++ b/docs/site/operations/templates/svg-samples/qr-only-24mm.svg @@ -0,0 +1,5 @@ + + qr-only-24mm — tape 24mm — 0 text lines + + + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/snipeit-12mm.svg b/docs/site/operations/templates/svg-samples/snipeit-12mm.svg new file mode 100644 index 0000000..0a1bb4c --- /dev/null +++ b/docs/site/operations/templates/svg-samples/snipeit-12mm.svg @@ -0,0 +1,7 @@ + + snipeit-12mm — tape 12mm — 2 text lines + + + ASSET-2024-001 + Dell Latitude 7430 + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/snipeit-18mm.svg b/docs/site/operations/templates/svg-samples/snipeit-18mm.svg new file mode 100644 index 0000000..f954280 --- /dev/null +++ b/docs/site/operations/templates/svg-samples/snipeit-18mm.svg @@ -0,0 +1,8 @@ + + snipeit-18mm — tape 18mm — 3 text lines + + + ASSET-2024-001 + Dell Latitude 7430 + IT Office | Bjoern Strausmann + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/snipeit-24mm.svg b/docs/site/operations/templates/svg-samples/snipeit-24mm.svg new file mode 100644 index 0000000..8d4726b --- /dev/null +++ b/docs/site/operations/templates/svg-samples/snipeit-24mm.svg @@ -0,0 +1,8 @@ + + snipeit-24mm — tape 24mm — 3 text lines + + + ASSET-2024-001 + Dell Latitude 7430 + IT Office | Bjoern Strausmann + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/spoolman-12mm.svg b/docs/site/operations/templates/svg-samples/spoolman-12mm.svg new file mode 100644 index 0000000..150a7d9 --- /dev/null +++ b/docs/site/operations/templates/svg-samples/spoolman-12mm.svg @@ -0,0 +1,7 @@ + + spoolman-12mm — tape 12mm — 2 text lines + + + PLA-Black-1kg + Spool #7 + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/spoolman-18mm.svg b/docs/site/operations/templates/svg-samples/spoolman-18mm.svg new file mode 100644 index 0000000..7bf828e --- /dev/null +++ b/docs/site/operations/templates/svg-samples/spoolman-18mm.svg @@ -0,0 +1,8 @@ + + spoolman-18mm — tape 18mm — 3 text lines + + + PLA-Black-1kg + Spool #7 + 780g left | Prusament + \ No newline at end of file diff --git a/docs/site/operations/templates/svg-samples/spoolman-24mm.svg b/docs/site/operations/templates/svg-samples/spoolman-24mm.svg new file mode 100644 index 0000000..8653868 --- /dev/null +++ b/docs/site/operations/templates/svg-samples/spoolman-24mm.svg @@ -0,0 +1,8 @@ + + spoolman-24mm — tape 24mm — 3 text lines + + + PLA-Black-1kg + Spool #7 + 780g left | Prusament + \ No newline at end of file