fix: TTF font + last_page→feed propagation (Phase 1i smoke-test live-bugs)#100
Conversation
…ored)
Container fehlte fonts-dejavu-core → ImageFont.truetype('DejaVuSans.ttf', N)
schlug mit OSError fehl → _load_font_cached fiel auf load_default() zurück.
Pillow's load_default() ist eine fixe-Größe Bitmap-Font die den size-Parameter
ignoriert — alle Templates renderten identisch große Texte unabhängig von font_size.
Fix: fonts-dejavu-core apt-Paket in runtime-Stage installieren.
Pillow findet /usr/share/fonts/truetype/dejavu/DejaVuSans.ttf via Pfad-Suche.
Test: test_label_renderer_uses_truetype_font verifiziert FreeTypeFont-Typ und
tatsächliche Größenskalierung (h22 > h10 * 1.5x).
Refs Phase 1i smoke-test live-bugs.
…etween batch items) batch_dispatch berechnete use_last_page=False für Intermediate-Items korrekt, aber der Wert verschwand in PTouchBackend.print_image mit # noqa: ARG002. _ptouch_print hatte keinen last_page-Parameter und übergab kein feed= an ptouch-py → Default feed=True → 22.5mm Pre-Roll vor jedem Batch-Item statt ~5mm Halbschnitt (wie Brother iOS App). Fix: - _ptouch_print: last_page: bool = True Parameter hinzugefügt - _ptouch_print: printer.print(..., feed=last_page) übergeben - PTouchBackend.print_image: # noqa: ARG002 entfernt, last_page=last_page durchgereicht - TypeError-Fallback (ältere ptouch-Lib ohne half_cut/feed): unverändert last_page=False (Intermediate) → feed=False → ~5mm Half-Cut Separation last_page=True (Last/Default) → feed=True → voller Tape-Feed Tests: 3 neue Tests verifizieren Propagation auf beiden Ebenen (print_image + _ptouch_print direkt). 3 bestehende fake_print-Stubs um last_page-Parameter erweitert. Refs Phase 1i smoke-test live-bugs.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request addresses two critical bugs identified during live smoke testing. The first fix ensures consistent text rendering by providing the necessary TrueType font files in the container environment. The second fix resolves an issue where tape was being wasted due to incorrect feed settings in multi-label batches, by properly passing the 'last_page' status to the underlying printer driver. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request installs the fonts-dejavu-core package in the Dockerfile to ensure that DejaVuSans.ttf is available, preventing Pillow from falling back to a fixed-size bitmap font. It also introduces a last_page parameter to the ptouch backend to control tape feed during batch printing, and adds corresponding unit tests. The feedback suggests skipping the new font-scaling test on local developer machines if the font is not installed system-wide, ensuring the local TDD workflow is not disrupted outside of Docker.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| from app.services.label_renderer import _load_font_cached | ||
| from PIL import ImageFont | ||
|
|
||
| # Cache leeren damit der Test sauber von Null startet | ||
| _load_font_cached.cache_clear() | ||
|
|
||
| font = _load_font_cached(22) | ||
| assert isinstance(font, ImageFont.FreeTypeFont), ( | ||
| f"Erwartet FreeTypeFont (TTF), bekommen {type(font).__name__}. " | ||
| "Stelle sicher dass fonts-dejavu-core im Dockerfile installiert ist " | ||
| "(/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf muss existieren)." | ||
| ) |
There was a problem hiding this comment.
Der Test test_label_renderer_uses_truetype_font schlägt auf Entwickler-Rechnern (z. B. macOS/Windows) fehl, wenn die Schriftart DejaVuSans.ttf nicht systemweit installiert ist. Dies beeinträchtigt den lokalen TDD-Workflow außerhalb von Docker.
Um dies zu beheben, ohne die Validierung in der CI/Docker-Umgebung zu schwächen, können wir den Test überspringen (pytest.skip), falls die Schriftart fehlt, wir uns aber nicht in einer CI- oder Container-Umgebung befinden.
| from app.services.label_renderer import _load_font_cached | |
| from PIL import ImageFont | |
| # Cache leeren damit der Test sauber von Null startet | |
| _load_font_cached.cache_clear() | |
| font = _load_font_cached(22) | |
| assert isinstance(font, ImageFont.FreeTypeFont), ( | |
| f"Erwartet FreeTypeFont (TTF), bekommen {type(font).__name__}. " | |
| "Stelle sicher dass fonts-dejavu-core im Dockerfile installiert ist " | |
| "(/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf muss existieren)." | |
| ) | |
| from app.services.label_renderer import _load_font_cached | |
| from PIL import ImageFont | |
| import os | |
| # Cache leeren damit der Test sauber von Null startet | |
| _load_font_cached.cache_clear() | |
| font = _load_font_cached(22) | |
| if not isinstance(font, ImageFont.FreeTypeFont): | |
| is_ci_or_docker = os.path.exists("/.dockerenv") or os.environ.get("CI") == "true" | |
| if not is_ci_or_docker: | |
| pytest.skip("DejaVuSans.ttf ist lokal nicht verfügbar (außerhalb von Docker/CI übersprungen)") | |
| assert isinstance(font, ImageFont.FreeTypeFont), ( | |
| f"Erwartet FreeTypeFont (TTF), bekommen {type(font).__name__}. " | |
| "Stelle sicher dass fonts-dejavu-core im Dockerfile installiert ist " | |
| "(/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf muss existieren)." | |
| ) |
There was a problem hiding this comment.
Pull request overview
Fixes two live “Phase 1i” regressions in the backend: (1) Pillow was silently falling back to a fixed-size bitmap font in containers (ignoring font_size), and (2) last_page was not propagated to the ptouch library, causing unintended full tape feed between batch items.
Changes:
- Install
fonts-dejavu-corein the backend runtime Docker image soImageFont.truetype("DejaVuSans.ttf", size)succeeds andfont_sizeactually scales. - Propagate
last_pagethroughPTouchBackend.print_image→_ptouch_printand map it toptouch.LabelPrinter.print(feed=last_page). - Add unit tests covering TrueType usage/scaling and
last_page→feedforwarding behavior.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 2 comments.
| File | Description |
|---|---|
| backend/tests/unit/services/test_label_renderer.py | Adds a unit test ensuring a real TTF font is used and font_size affects glyph size. |
| backend/tests/unit/printer_backends/test_ptouch_backend.py | Updates existing stubs and adds tests verifying last_page is forwarded and translated to feed. |
| backend/Dockerfile | Installs fonts-dejavu-core in runtime stage to make DejaVuSans.ttf available in containers. |
| backend/app/printer_backends/ptouch_backend.py | Adds last_page parameter to _ptouch_print and forwards it from print_image to ptouch as feed. |
| Dieser Test schlägt im Container FEHL bis Dockerfile gefixt ist. | ||
| Auf Dev-Rechnern mit installierten Fonts läuft er grün. |
| ) | ||
| except TypeError: | ||
| # Older ptouch lib doesn't support half_cut — fall back to full cut | ||
| # Older ptouch lib doesn't support half_cut/feed — fall back to full cut |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #100 +/- ##
==========================================
- Coverage 89.57% 89.52% -0.05%
==========================================
Files 95 95
Lines 4316 4316
Branches 368 368
==========================================
- Hits 3866 3864 -2
- Misses 357 360 +3
+ Partials 93 92 -1
... and 3 files with indirect coverage changes
Flags with carried forward coverage won't be shown. Click here to find out more. Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
- test_label_renderer.py: pytest.skip wenn DejaVuSans nicht system-installiert UND nicht in CI/Container (Gemini medium-priority — lokaler TDD-Workflow auf macOS/Windows nicht stoeren). Path(...).exists() statt os.path.exists. - test_label_renderer.py: Docstring aktualisiert — Dockerfile-Fix ist im PR bereits drin, also nicht mehr 'failt im Container' sondern Erklaerung warum CI/Container der Test-Pflicht-Pfad ist (Copilot) - ptouch_backend.py: TypeError-Fallback-Kommentar erweitert um feed/last_page Verhalten bei Legacy-ptouch <1.1 (Copilot) Refs PR #100 review-followup
…forwarding (Phase 1k.2 Task 6) Bug discovered during Phase 1k.2 plan-writing: _PTPQueuePrinter.print_image forwarded only auto_cut + high_resolution. half_cut und last_page wurden aus options dict geholt aber NIE an backend.print_image() weitergeleitet. PR #100 fix landete nie bei ptouch lib. Fix: explicit options.pop für half_cut + last_page. Plus neue print_images() Adapter-Methode für Queue-BatchJob-Pfad. Refs #102, regression-fixes PR #100 (silent drop in adapter layer)
Closes #102. Multi-Label-Batches an PT-Series Drucker produzieren 5mm Half-Cut zwischen Labels via ptouch.print_multi statt 22.5mm Pre-Roll pro Item. Brother iOS App Verhalten. ## Architektur - BackendRouter + per-slug PrintService (Phase 1i) bleibt - Neue PrinterBackend.print_images Protocol-Methode - PTouchBackend.print_images via ptouch.LabelPrinter.print_multi (atomic) - BrotherQLBackend/MockBackend via default_print_images_loop (best-effort) - PrintQueue.BatchJob als neuer Queue-Item-Typ - Worker dispatched via isinstance, _process_batch ist atomic + state-consistent - batch_dispatch refactor: ONE BatchJob statt N PrintJobs ## Bonus Bug-Fix _PTPQueuePrinter Adapter droppte half_cut + last_page bei PR #100 — die Lib bekam den Fix nie. Task 6 fixt das mit Regression-Test. ## Reviews adressiert - Gemini R1: 3 architektur-Lücken (in-memory Job-Registrierung, JobStateMachine + SSE, parallele Renders) - Copilot R1: 9 design/quality findings (Protocol-Approach, Scope, Privacy, atomic docstring, PrinterError-Konsistenz, public API) - Gemini R2: 3 runtime-bugs (lookup_ql, active_jobs alignment, DbJob signature) ## Stats - 981 passed, 5 skipped (+32 net new tests) - 0 regressions - 80.04% diff coverage (codecov target met) - Privacy/lint/mypy clean Refs Phase 1k Umbrella #101, Phase 1k.1 Layout-Engine #103, Phase 1k.3 Editor #104</commit_message> </invoke>
## 0.9.0 (2026-06-05) * feat: Phase 1i — Label-Quality + Multi-Printer + Samla + Preview (#98) ([a33dda9](a33dda9)), closes [#98](#98) * feat: Phase 1k.2 Multi-Label-Batch via ptouch.print_multi (#106) ([a9726a9](a9726a9)), closes [#106](#106) [#101](#101) [#103](#103) * feat(api): GET /api/templates/{key}/preview-svg + Dashboard-Modal (Phase 1i D) ([c2cbf3c](c2cbf3c)) * feat(api): GET /api/templates/{key}/preview.png (Diagnose-Tool, Phase 1i A) ([27dd325](27dd325)), closes [#5](#5) * feat(batch): dispatch_batch refactor to atomic BatchJob + submit_batch_job (Phase 1k.2 Task 9) ([a2b6a98](a2b6a98)), closes [#102](#102) [#106](#106) * feat(batch): printer_slug + half_cut_override + BackendRouter-Lookup (Phase 1i C) ([0561504](0561504)), closes [#6](#6) * feat(brother_ql): print_images via default_print_images_loop (Phase 1k.2 Task 4) ([f56e44c](f56e44c)), closes [#102](#102) * feat(config): Clean-Break Settings + upsert_runtime_printers (Phase 1i H CA-1 7a) ([64c38b3](64c38b3)) * feat(lifespan): Multi-Printer-Wiring + service_for pro Drucker (Phase 1i H CA-1 7b) ([3661a4e](3661a4e)) * feat(mock): MockBackend.print_images via default_print_images_loop (Phase 1k.2 Task 5) ([4ebdbee](4ebdbee)), closes [#102](#102) * feat(printer_backends): BrotherQLBackend + QLSeriesModel (Phase 1i G) ([ad52009](ad52009)) * feat(printer_backends): default_print_images_loop helper (Phase 1k.2 Task 1) ([54e1123](54e1123)), closes [#102](#102) * feat(printer_backends): half_cut Parameter + half_cut_supported Capability (Phase 1i C) ([d41c246](d41c246)) * feat(printer_backends): PrinterBackend.print_images Protocol method (Phase 1k.2 Task 2) ([e93f945](e93f945)), closes [#102](#102) * feat(pt): _PTPQueuePrinter.print_images + bug-fix half_cut/last_page forwarding (Phase 1k.2 Task 6) ([15ef310](15ef310)), closes [#100](#100) [#100](#100) * feat(ptouch): print_images via ptouch.print_multi (Phase 1k.2 Task 3) ([c0d15f0](c0d15f0)), closes [#102](#102) * feat(ql): _QLQueuePrinter.print_images adapter (Phase 1k.2 Task 7) ([cb1e317](cb1e317)), closes [#102](#102) [#106](#106) * feat(queue): BatchJob + enqueue_batch + worker isinstance dispatch (Phase 1k.2 Task 8) ([c08da56](c08da56)), closes [#102](#102) [#106](#106) * feat(schemas): PrinterYAMLConfig + PrintersFile (Phase 1i H) ([7779a94](7779a94)) * feat(scripts): smoke_first_print_batch.py — manual 4-item batch hardware-smoke (Phase 1k.2 Task 12) ([d2ce28e](d2ce28e)), closes [#102](#102) * feat(services): BackendRouter mit slug->Backend-Mapping (Phase 1i H CA-2) ([a735a34](a735a34)) * feat(services): PrinterConfigLoader + printers.yaml.example (Phase 1i H) ([f065575](f065575)) * feat(templates): DPI-Fix + Samla-Templates (Phase 1i B) ([af7bce8](af7bce8)) * test(batch): end-to-end multi-label batch integration test (Phase 1k.2 Task 11) ([bcc1d04](bcc1d04)), closes [#102](#102) * test(queue): coverage for _process_batch error paths + pragma on _ptouch_print_multi (Phase 1k.2 Tas ([0b6250d](0b6250d)), closes [#106](#106) [#102](#102) * style: ruff format + lint cleanup (Phase 1i) ([6cffe4c](6cffe4c)) * style: ruff format Task 11 integration test (Phase 1k.2 cleanup) ([ad47f7e](ad47f7e)), closes [#102](#102) * fix: TTF font + last_page→feed (Phase 1i smoke-test bugs) (#100) ([3998575](3998575)), closes [#100](#100) * fix(api): preview-png statt preview.png (OpenAPI kebab-Konvention) ([b420722](b420722)) * fix(ci): mypy types + privacy scan (Phase 1i) ([541502e](541502e)) * fix(ci): SnmpQueryError __all__ export + privacy test IP ([99c1946](99c1946)), closes [#98](#98) [#98](#98) * fix(ci): UUID-Continuity-Runbook entfernen (privacy) ([b027cff](b027cff)) * fix(lifespan): col() for mypy-safe SQLModel where-clause in slug collision check ([6f9d840](6f9d840)), closes [#98](#98) * fix(lifespan): session.flush() + slug-collision-detection in upsert_runtime_printers ([e7d81e6](e7d81e6)), closes [#98](#98) * fix(print_service): forward auto_cut/high_resolution from request options (Phase 1k.2 Task 9 follow- ([51539b5](51539b5)), closes [#102](#102) * fix(printer_backends): BrotherQLBackend.preflight_check (Phase 1i Task 8b) ([dba9b6c](dba9b6c)) * fix(printer_backends): print_images stubs to fix Protocol isinstance regressions (Phase 1k.2 Task 2 ([5cbf955](5cbf955)), closes [#102](#102) * fix(ptouch): propagate last_page→feed to ptouch lib (short half-cut between batch items) ([05f780d](05f780d)) * fix(queue): align image payloads with active_jobs + strengthen Task 8 tests (Phase 1k.2 Task 8 follo ([c658e87](c658e87)), closes [#102](#102) [#106](#106) * fix(renderer): install fonts-dejavu-core in Dockerfile (font_size honored) ([9b0cd84](9b0cd84)) * fix(reviews): adresse Gemini + Copilot PR #100 findings ([9507432](9507432)), closes [#100](#100) [#100](#100) * fix(schemas): half_cut_override docstring matches actual force-to-false behavior ([a7b84b9](a7b84b9)), closes [#98](#98) * docs: pydantic-settings honesty + DPI-decouple + openapi range + preview-png drift ([3d8857e](3d8857e)), closes [PR#98-Copilot](https://github.com/PR/issues/98-Copilot) [PR#98-Copilot](https://github.com/PR/issues/98-Copilot) [PR#98-Copilot](https://github.com/PR/issues/98-Copilot) [PR#98-Copilot](https://github.com/PR/issues/98-Copilot) [#98](#98) * docs(plan): adresse Gemini Round-2 Review G-R2-1/2/3 (PR #106) ([968b693](968b693)), closes [#106](#106) [#102](#102) [#106](#106) * docs(plan): adresse Gemini-Review G1+G2+G3 (PR #106) ([ad3aec5](ad3aec5)), closes [#106](#106) [#102](#102) [#106](#106) * docs(plan): Phase 1k.2 Multi-Label-Batch implementation plan (#102) ([8917d0e](8917d0e)), closes [#102](#102) [#100](#100) [#102](#102) * docs(research): PT-P750W Layout-Diagnose Befund (Phase 1i A) ([06e8fa9](06e8fa9)) * docs(runbook): UUID-Continuity-Check Phase 1i (MA-2-Fix) ([0ef3861](0ef3861)) * docs(spec): Phase 1k.2 Multi-Label-Batch via ptouch.print_multi (#102) ([96e53c9](96e53c9)), closes [#102](#102) [#101](#101) [#102](#102) [#103](#103) [#102](#102) * docs(spec+plan): adresse Copilot-Review C1-C9 + Privacy-Fixes (PR #106) ([4f56a2a](4f56a2a)), closes [#106](#106) [#102](#102) [#106](#106) * Merge pull request #99 from strausmann/fix/phase1i-ql-preflight-check ([8df1be3](8df1be3)), closes [#99](#99) [skip ci]
Bug 1: font_size wird ignoriert
Symptom (Live): Templates mit unterschiedlichen
font_sizerendern visuell identisch große Texte.Root Cause: Container hat keine TrueType-Fonts installiert.
ImageFont.truetype('DejaVuSans.ttf', N)schlägt fehl mitOSError('cannot open resource')._load_font_cachedfällt aufImageFont.load_default()zurück — Pillow's Default ist eine fixe-Größe Bitmap-Font die dassize-Argument ignoriert.Fix:
fonts-dejavu-coreapt-Paket in der runtime-Stage des Dockerfiles installieren →/usr/share/fonts/truetype/dejavu/DejaVuSans.ttfverfügbar, Pillow findet es via Pfad-Suche.Test:
test_label_renderer_uses_truetype_fontverifiziert FreeTypeFont-Typ und tatsächliche Größenskalierung (Glyphe bei size=22 muss >1.5x größer sein als bei size=10).Bug 2:
last_pagewird weggeworfen → 22.5mm Pre-Roll überallSymptom (Live): Multi-Label-Batch mit half_cut=True erzeugt 22.5mm Tape-Vorlauf vor jedem Label statt ~5mm Half-Cut (wie Brother iOS App).
Root Cause:
ptouch-py 1.1.0 LabelPrinter.print()hat einenfeed: bool = TrueParameter der den Tape-Feed nach dem Print steuert._ptouch_printinptouch_backend.pyakzeptiertelast_pagenicht und übergab keinfeedan die ptouch-Lib → Defaultfeed=True→ voller Pre-Roll für JEDES Item im Batch.batch_dispatch.pyberechneteuse_last_pagekorrekt (False für alle außer letztem Item), aber der Wert verschwand inPTouchBackend.print_imagemit# noqa: ARG002(unused arg) Marker.Fix:
_ptouch_printSignature:last_page: bool = TrueParameter hinzugefügt_ptouch_printBody:printer.print(..., feed=last_page)übergebenPTouchBackend.print_image:# noqa: ARG002entfernt,last_page=last_pagedurchgereichtlast_page=False(Intermediate-Item) →feed=False→ ~5mm Half-Cut Trennunglast_page=True(Last-Item/Default) →feed=True→ voller Tape-Feed für TrennungTests
test_label_renderer_uses_truetype_font— verifiziert FreeTypeFont + Skalierungtest_print_image_passes_last_page_false_to_ptouch,test_ptouch_print_calls_printer_with_feed_false,test_ptouch_print_calls_printer_with_feed_true_by_defaultfake_print-Stubs umlast_page-Parameter erweitert (hatten keinen Platz für das neue kwarg)Gesamt: 949 passed (Baseline 945, +4 neue Tests), 5 skipped, ruff + mypy clean.
Refs Phase 1i live smoke-test bugs.