diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..80c806c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +# Preserve verbatim dependency patch context and recorded tool output. +patches/*.patch -whitespace +openspec/changes/archive/**/evidence/*.txt -whitespace diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d16d109..9634c55 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -12,6 +12,19 @@ jobs: steps: - uses: actions/checkout@v4 - uses: docker/setup-buildx-action@v3 + - name: Build and test runtime image + uses: docker/build-push-action@v6 + with: + context: . + file: Dockerfile.deploy + platforms: linux/amd64 + load: true + tags: ptouch-deploy:check + - name: Smoke - IPP jobs and isolated socket capture + run: | + set -e + sudo apt-get update && sudo apt-get install -y --no-install-recommends cups-ipp-utils + python3 scripts/smoke-image.py ptouch-deploy:check - name: Log in to GHCR if: github.event_name != 'pull_request' uses: docker/login-action@v4 @@ -28,34 +41,6 @@ jobs: tags: | type=raw,value=edge,enable=${{ github.ref == 'refs/heads/main' }} type=sha - - name: Build (PR = load, no push) - if: github.event_name == 'pull_request' - uses: docker/build-push-action@v6 - with: - context: . - file: Dockerfile.deploy - platforms: linux/amd64 - load: true - tags: ptouch-deploy:pr - - name: Smoke (PR) - no hardware, no sink (Codex #2/#9) - if: github.event_name == 'pull_request' - run: | - set -e - sudo apt-get update && sudo apt-get install -y --no-install-recommends cups-ipp-utils - cleanup() { docker rm -f ptouch-smoke 2>/dev/null || true; } - trap cleanup EXIT - # socket:// registers the printer without a listener; attributes query needs no sink - docker run -d --name ptouch-smoke -p 18000:8000 \ - -e PTOUCH_DEVICE_URI=socket://127.0.0.1:9100 ptouch-deploy:pr - for i in $(seq 1 15); do - if ipptool -t ipp://127.0.0.1:18000/ipp/print/ptouch \ - ./spikes/fixtures/get-printer-attributes.test >/tmp/ptouch-smoke-ipp.log 2>&1; then - echo "IPP OK"; break; fi - sleep 1 - done - ipptool -tv ipp://127.0.0.1:18000/ipp/print/ptouch \ - ./spikes/fixtures/get-printer-attributes.test - docker logs ptouch-smoke - name: Build + push (main) if: github.event_name != 'pull_request' uses: docker/build-push-action@v6 diff --git a/CMakeLists.txt b/CMakeLists.txt index f89759c..6221233 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,6 +4,7 @@ project(ptouch_print_server C) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) add_compile_options(-Wall -Wextra) +option(PTOUCH_CORE_ONLY "Build only the hardware-free core and capture tests" OFF) # Pure, hardware-free core (libc only). The PAPPL + raw-libusb adapter (M2) links this. add_library(ptcore STATIC @@ -18,34 +19,61 @@ target_include_directories(ptcore PUBLIC src) enable_testing() add_subdirectory(tests) -# The PAPPL app (M2+) builds only where PAPPL + libusb + libpng are present (the container). -# On the Mac (no PAPPL) the pure core + tests still build. -find_package(PkgConfig) -if(PkgConfig_FOUND) - pkg_check_modules(PAPPL pappl) - pkg_check_modules(USB libusb-1.0) - pkg_check_modules(PNG libpng) # our image/png filter (#40) calls libpng directly - if(PAPPL_FOUND AND USB_FOUND) - # Optional live transport for the test-only probe, using the existing scheme. - target_sources(ptouch-cut-probe PRIVATE src/device_usb.c) - target_compile_definitions(ptouch-cut-probe PRIVATE PT_PROBE_TRANSPORT=1) - target_link_libraries(ptouch-cut-probe PRIVATE ${PAPPL_LIBRARIES} ${USB_LIBRARIES}) - target_include_directories(ptouch-cut-probe PRIVATE ${PAPPL_INCLUDE_DIRS} ${USB_INCLUDE_DIRS}) - target_link_directories(ptouch-cut-probe PRIVATE ${PAPPL_LIBRARY_DIRS} ${USB_LIBRARY_DIRS}) - target_compile_options(ptouch-cut-probe PRIVATE ${PAPPL_CFLAGS_OTHER}) - endif() - if(PAPPL_FOUND AND USB_FOUND AND PNG_FOUND) - add_executable(test_driver tests/test_driver.c src/filter_png.c) - target_link_libraries(test_driver PRIVATE ptcore ${PAPPL_LIBRARIES} ${PNG_LIBRARIES}) - target_include_directories(test_driver PRIVATE ${PAPPL_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS}) - target_link_directories(test_driver PRIVATE ${PAPPL_LIBRARY_DIRS} ${PNG_LIBRARY_DIRS}) - target_compile_options(test_driver PRIVATE ${PAPPL_CFLAGS_OTHER}) - add_test(NAME driver_finishings COMMAND test_driver) - set_tests_properties(driver_finishings PROPERTIES TIMEOUT 90) - add_executable(ptouch-app src/main.c src/driver.c src/device_usb.c src/filter_png.c) - target_link_libraries(ptouch-app PRIVATE ptcore ${PAPPL_LIBRARIES} ${USB_LIBRARIES} ${PNG_LIBRARIES}) - target_include_directories(ptouch-app PRIVATE ${PAPPL_INCLUDE_DIRS} ${USB_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS}) - target_link_directories(ptouch-app PRIVATE ${PAPPL_LIBRARY_DIRS} ${USB_LIBRARY_DIRS} ${PNG_LIBRARY_DIRS}) - target_compile_options(ptouch-app PRIVATE ${PAPPL_CFLAGS_OTHER}) - endif() +# The default build must include the app and adapter tests. Opt into the core +# explicitly when building on a machine without the printer dependencies. +if(NOT PTOUCH_CORE_ONLY) + find_package(PkgConfig REQUIRED) + pkg_check_modules(PAPPL REQUIRED pappl) + pkg_check_modules(USB REQUIRED libusb-1.0) + pkg_check_modules(PNG REQUIRED libpng) + # Optional live transport for the test-only probe, using the existing scheme. + target_sources(ptouch-cut-probe PRIVATE src/device_usb.c) + target_compile_definitions(ptouch-cut-probe PRIVATE PT_PROBE_TRANSPORT=1) + target_link_libraries(ptouch-cut-probe PRIVATE ${PAPPL_LIBRARIES} ${USB_LIBRARIES}) + target_include_directories(ptouch-cut-probe PRIVATE ${PAPPL_INCLUDE_DIRS} ${USB_INCLUDE_DIRS}) + target_link_directories(ptouch-cut-probe PRIVATE ${PAPPL_LIBRARY_DIRS} ${USB_LIBRARY_DIRS}) + target_compile_options(ptouch-cut-probe PRIVATE ${PAPPL_CFLAGS_OTHER}) + add_executable(test_driver tests/test_driver.c src/filter_png.c) + target_link_libraries(test_driver PRIVATE ptcore ${PAPPL_LIBRARIES} ${PNG_LIBRARIES}) + target_include_directories(test_driver PRIVATE ${PAPPL_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS}) + target_link_directories(test_driver PRIVATE ${PAPPL_LIBRARY_DIRS} ${PNG_LIBRARY_DIRS}) + target_compile_options(test_driver PRIVATE ${PAPPL_CFLAGS_OTHER}) + add_test(NAME driver_finishings COMMAND test_driver) + set_tests_properties(driver_finishings PROPERTIES TIMEOUT 90) + add_executable(test_pappl_raster tests/test_pappl_raster.c) + target_link_libraries(test_pappl_raster PRIVATE ${PAPPL_LIBRARIES} ${PNG_LIBRARIES}) + target_include_directories(test_pappl_raster PRIVATE ${PAPPL_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS}) + target_link_directories(test_pappl_raster PRIVATE ${PAPPL_LIBRARY_DIRS} ${PNG_LIBRARY_DIRS}) + target_compile_options(test_pappl_raster PRIVATE ${PAPPL_CFLAGS_OTHER}) + add_test(NAME pappl_raster COMMAND test_pappl_raster) + set_tests_properties(pappl_raster PROPERTIES TIMEOUT 90) + add_executable(test_pappl_finishings tests/test_pappl_finishings.c) + target_link_libraries(test_pappl_finishings PRIVATE ${PAPPL_LIBRARIES}) + target_include_directories(test_pappl_finishings PRIVATE ${PAPPL_INCLUDE_DIRS}) + target_link_directories(test_pappl_finishings PRIVATE ${PAPPL_LIBRARY_DIRS}) + target_compile_options(test_pappl_finishings PRIVATE ${PAPPL_CFLAGS_OTHER}) + add_test(NAME pappl_finishings COMMAND test_pappl_finishings) + set_tests_properties(pappl_finishings PROPERTIES TIMEOUT 90) + add_executable(test_png_geometry tests/test_png_geometry.c src/driver.c src/filter_png.c) + target_link_libraries(test_png_geometry PRIVATE ptcore ${PAPPL_LIBRARIES} ${PNG_LIBRARIES}) + target_include_directories(test_png_geometry PRIVATE ${PAPPL_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS}) + target_link_directories(test_png_geometry PRIVATE ${PAPPL_LIBRARY_DIRS} ${PNG_LIBRARY_DIRS}) + target_compile_options(test_png_geometry PRIVATE ${PAPPL_CFLAGS_OTHER}) + add_test(NAME png_geometry COMMAND test_png_geometry) + set_tests_properties(png_geometry PROPERTIES TIMEOUT 90) + add_executable(test_device_usb tests/test_device_usb.c) + target_link_libraries(test_device_usb PRIVATE ptcore ${PAPPL_LIBRARIES} ${USB_LIBRARIES}) + target_include_directories(test_device_usb PRIVATE ${PAPPL_INCLUDE_DIRS} ${USB_INCLUDE_DIRS}) + target_link_directories(test_device_usb PRIVATE ${PAPPL_LIBRARY_DIRS} ${USB_LIBRARY_DIRS}) + target_compile_options(test_device_usb PRIVATE ${PAPPL_CFLAGS_OTHER}) + add_test(NAME device_usb COMMAND test_device_usb) + # Test assertions include fixture setup and must stay active in Release too. + foreach(test_target test_driver test_pappl_raster test_pappl_finishings test_png_geometry test_device_usb) + target_compile_options(${test_target} PRIVATE -UNDEBUG) + endforeach() + add_executable(ptouch-app src/main.c src/driver.c src/device_usb.c src/filter_png.c) + target_link_libraries(ptouch-app PRIVATE ptcore ${PAPPL_LIBRARIES} ${USB_LIBRARIES} ${PNG_LIBRARIES}) + target_include_directories(ptouch-app PRIVATE ${PAPPL_INCLUDE_DIRS} ${USB_INCLUDE_DIRS} ${PNG_INCLUDE_DIRS}) + target_link_directories(ptouch-app PRIVATE ${PAPPL_LIBRARY_DIRS} ${USB_LIBRARY_DIRS} ${PNG_LIBRARY_DIRS}) + target_compile_options(ptouch-app PRIVATE ${PAPPL_CFLAGS_OTHER}) endif() diff --git a/Dockerfile b/Dockerfile index c04a398..a9b658f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,14 +7,22 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ autoconf automake libtool gettext autopoint \ libusb-1.0-0-dev libgd-dev libpng-dev libjpeg-dev zlib1g-dev \ libcups2-dev libcupsimage2-dev cups-ipp-utils usbutils \ - libssl-dev libavahi-client-dev \ + libssl-dev libavahi-client-dev libgnutls28-dev libkrb5-dev \ python3 python3-pil \ && rm -rf /var/lib/apt/lists/* # PAPPL from source (not packaged in bookworm). Pinned to the 1.4.x line, which # targets CUPS 2.x (bookworm's libcups2-dev 2.4); master needs libcups >= 2.5. +# Retain Debian security fixes while correcting only raster EOF reporting. +COPY patches/cups-2.4.2-deb12u9-raster.patch /tmp/cups-raster.patch +COPY scripts/build-cups.sh /tmp/build-cups.sh +RUN /tmp/build-cups.sh /tmp/cups-raster.patch +COPY patches/pappl-1.4.11-raster.patch /tmp/pappl-raster.patch +COPY patches/pappl-1.4.11-finishings.patch /tmp/pappl-finishings.patch RUN git clone --depth 1 --branch v1.4.11 https://github.com/michaelrsweet/pappl /tmp/pappl \ - && cd /tmp/pappl && ./configure && make && make install && ldconfig \ + && cd /tmp/pappl && patch --batch --forward --fuzz=0 -p1 < /tmp/pappl-raster.patch \ + && patch --batch --forward --fuzz=0 -p1 < /tmp/pappl-finishings.patch \ + && ./configure && make && make install && ldconfig \ && rm -rf /tmp/pappl WORKDIR /work diff --git a/Dockerfile.deploy b/Dockerfile.deploy index d682041..1a865b8 100644 --- a/Dockerfile.deploy +++ b/Dockerfile.deploy @@ -2,16 +2,25 @@ # ---- build stage: PAPPL + ptouch-app ---- FROM debian:bookworm AS build RUN apt-get update && apt-get install -y --no-install-recommends \ - build-essential cmake pkg-config git ca-certificates \ + build-essential cmake pkg-config git ca-certificates python3 \ libusb-1.0-0-dev libpng-dev libjpeg-dev zlib1g-dev \ - libcups2-dev libcupsimage2-dev libssl-dev libavahi-client-dev \ + libcups2-dev libcupsimage2-dev libssl-dev libavahi-client-dev libgnutls28-dev libkrb5-dev \ && rm -rf /var/lib/apt/lists/* +# Retain Debian security fixes while correcting only raster EOF reporting. +COPY patches/cups-2.4.2-deb12u9-raster.patch /tmp/cups-raster.patch +COPY scripts/build-cups.sh /tmp/build-cups.sh +RUN /tmp/build-cups.sh /tmp/cups-raster.patch +COPY patches/pappl-1.4.11-raster.patch /tmp/pappl-raster.patch +COPY patches/pappl-1.4.11-finishings.patch /tmp/pappl-finishings.patch RUN git clone --depth 1 --branch v1.4.11 https://github.com/michaelrsweet/pappl /tmp/pappl \ - && cd /tmp/pappl && ./configure && make && make install && ldconfig \ + && cd /tmp/pappl && patch --batch --forward --fuzz=0 -p1 < /tmp/pappl-raster.patch \ + && patch --batch --forward --fuzz=0 -p1 < /tmp/pappl-finishings.patch \ + && ./configure && make && make install && ldconfig \ && rm -rf /tmp/pappl WORKDIR /src COPY . . -RUN cmake -S . -B build-app && cmake --build build-app --target ptouch-app \ +RUN cmake -S . -B build-app -DPTOUCH_CORE_ONLY=OFF && cmake --build build-app \ + && ctest --test-dir build-app --output-on-failure \ && install -Dm755 build-app/ptouch-app /out/usr/local/bin/ptouch-app # ---- runtime stage: slim ---- @@ -21,7 +30,13 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ libpng16-16 libjpeg62-turbo zlib1g libssl3 \ avahi-daemon dbus ca-certificates \ && rm -rf /var/lib/apt/lists/* -# PAPPL shared libs (SONAME chain) + the app +# Patched ABI-compatible CUPS and PAPPL libraries (including SONAME links). +COPY --from=build /usr/local/lib/libcups.so* /usr/local/lib/ +COPY --from=build /usr/local/lib/libcupsimage.so* /usr/local/lib/ +COPY --from=build /usr/local/share/ptouch/cups-source-version /usr/local/share/ptouch/cups-source-version +# Fail rather than replace a newer runtime security revision with cached libraries. +RUN test "$(dpkg-query -W -f='${source:Version}' libcups2)" = "$(cat /usr/local/share/ptouch/cups-source-version)" +# PAPPL shared libs + the app COPY --from=build /usr/local/lib/libpappl.so* /usr/local/lib/ COPY --from=build /out/usr/local/bin/ptouch-app /usr/local/bin/ptouch-app RUN ldconfig diff --git a/README.md b/README.md index 1a4f069..c44a572 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ The Brother raster protocol and device table are reused from ## Status -Working and deployed. Only the **Brother PT-2730** is hardware-certified; the -print path emits PT-2730 protocol for every device. +Working and deployed. Only the **Brother PT-2730** is print-supported and +hardware-certified; other recognized models do not receive print output. | Milestone | Scope | State | |-----------|-------|-------| @@ -25,15 +25,15 @@ print path emits PT-2730 protocol for every device. What it does today: -- Prints 1:1 at the tape's imageable width, with the label length taken from the - submitted image rather than a fixed page. +- Prints 1:1 within the tape's imageable width, honoring submitted page geometry; + PNG length follows its image when a concrete media length is omitted. - Advertises **nominal** tape sizes with real margins (`om_24x100mm`), so clients match the tape they actually loaded. -- Uses each job's IPP **finishings** choice: `none` leaves tape uncut and keeps - the run open; `trim` cuts its labels and finalizes the last one. Queue contents - and idle time do not choose whether to cut. +- Uses standard IPP **finishings** for cuts between labels and at batch end, + including an advertised chain-only collection template. Queue contents and + idle time do not choose whether to cut. - Rejects a job whose width does not match the loaded tape, before anything is - sent to the printer. + sent as page setup or raster to the printer. - Reports `offline` when the printer is switched off instead of disappearing, and recovers on its own when it comes back - queued jobs then print. @@ -64,10 +64,12 @@ Three layers: `src/tables.c` recognizes the P-touch USB ids below (transcribed from upstream `ptdevs[]`). **Only the PT-2730 is print-supported and hardware-certified.** Every -other entry is recognized and discoverable, but is **not print-supported**: the -raster path emits PT-2730 protocol (uncompressed, 128-dot head) regardless of the -matched model, and per-model flag dispatch (PackBits, P700 init) does not exist -yet. All known models are 180 dpi / 128 px max. +other entry remains recognition metadata and is **not print-supported**: discovery +reports it in diagnostics without offering a printable device. Per-model flag +dispatch (PackBits, P700 init) does not exist yet. All known models are 180 dpi / +128 px max. `PTOUCH_MODEL` defaults to `PT-2730`; unknown or unsupported values +fail before discovery. Multiple matching PT-2730 units block printing as ambiguous, +while a single unit can be unplugged and reattached without replacing its queue. | Model | USB id | flags | Print-supported | |-------|--------|-------|-----------------| @@ -90,79 +92,140 @@ To add a model, see [docs/ADDING-A-MODEL.md](docs/ADDING-A-MODEL.md). ## Build and run -The pure C core builds and unit-tests anywhere with CMake and a C11 compiler. No -hardware or PAPPL needed: +The hardware-free core requires CMake, a C11 compiler, and Python 3 for the +capture suite. Select core-only mode explicitly; PAPPL and USB are not needed: ```bash -cmake -S . -B build && cmake --build build && ctest --test-dir build +cmake -S . -B build-core -DPTOUCH_CORE_ONLY=ON +cmake --build build-core +ctest --test-dir build-core --output-on-failure ``` -This builds `ptcore` and runs the five core test suites (tables, protocol, -raster, status, cutter). +This builds `ptcore` and the command probe, then runs the five core suites +(tables, protocol, raster, status, cutter) and the Python capture suite. -The full `ptouch-app` executable builds only where PAPPL and libusb-1.0 are -present (`pkg-config` finds `pappl` and `libusb-1.0`). On a machine without them, -CMake skips the app target and still builds the core and tests. The app is built -and exercised against PT-2730 hardware in a container; see `Dockerfile`. +The default full build requires `pkg-config` packages `pappl`, `libusb-1.0`, +and `libpng`, in addition to the core test tools. Missing dependencies fail +configuration. It builds `ptouch-app` and the adapter tests as well as the core: + +```bash +cmake -S . -B build-app -DPTOUCH_CORE_ONLY=OFF +cmake --build build-app +ctest --test-dir build-app --output-on-failure +``` + +`Dockerfile` supplies the development dependencies; `Dockerfile.deploy` builds +all targets and runs CTest before installing the app into the runtime image. +These tests use no printer; physical behavior requires separate PT-2730 checks. ### Printing notes for clients -The printer advertises continuous-roll media (tape-width-fixed, length-variable -`roll_min`/`roll_max` range) and uses `print-scaling=none`, so a label prints -1:1. For a label to come out at its true size, a client must: - -- send the image at the printer resolution, **180 dpi** (a PNG with no DPI - metadata is read as 180 dpi), and -- match the loaded tape across its width. The printer reports the tape via - `media-ready` at its **nominal** width, with the unprintable edge expressed as - left/right margins (a 24 mm tape images ~18 mm, the 128-dot head); a job whose - rendered width does not match the loaded tape is refused. - -**Label length follows the PNG.** A submitted `image/png` prints at the physical -length of the image itself (its along-the-tape pixel count divided by its DPI), -so no `media-col` length is needed and there are no blank runs before and after -the content. Lengths below the 5 mm roll minimum are padded up to it; anything -over the 300 mm roll maximum is refused rather than cropped. JPEG is unaffected: -it still goes through PAPPL's built-in filter and prints on the fixed media -length. +Media dimensions describe the physical tape, and `media-ready` publishes the +loaded nominal width with its physical left/right margins. The supported tape +widths are 6, 9, 12, 18, and 24 mm. A page whose width does not match the loaded +tape is refused before raster output. The default `print-scaling=none` preserves +physical size; explicit standard scaling requests use PAPPL's placement policy. +The server does not silently choose fit or crop to rescue a mismatched design. + +For PNG at `print-scaling=none`, use the printer's 180 dpi resolution. PNGs +without resolution metadata are interpreted as 180 dpi; square DPI metadata +sets the physical size, while non-square DPI is rejected. Cross-tape content +must fit the imageable width (32, 52, 76, 120, or 128 printer dots respectively). +A full nominal-width canvas is also accepted when its non-imageable edge +margins are entirely white after transparency is composited. Nonblank physical +margins, oversized content, or content longer than an explicitly selected page +are rejected before page raster rather than silently clipped. + +An explicit named `media` size, `media-col.media-size-name`, or +`media-col.media-size.y-dimension` determines +PNG page length. For example, a 180-pixel image at 180 dpi on explicitly selected +50 mm media prints a 50 mm page, with centered content. If concrete length is +omitted, PNG length follows its oriented along-tape pixel dimension and DPI. +Source-only or margin-only `media-col` does not suppress that convenience. +Automatic lengths below 5 mm are padded to 5 mm; lengths above 300 mm are refused. +PNG's automatic orientation uses portrait for tall/square images and +reverse-landscape (90 degrees clockwise) for wide images. Explicit standard +`orientation-requested` overrides it. + +JPEG uses PAPPL's standard image placement and selected/default media length, +without PNG's automatic-length convenience or source-content preflight. Standard +PWG raster and URF retain each submitted page's geometry. Explicit page dimensions +have the same physical meaning across formats; contradictory raster/job dimensions +fail visibly. PNG is advertised once as `image/png`, without a private raster MIME. + +Custom lengths also use standard `media-col` dimensions. The roll declarations +accept width rounding of ±0.01 mm around each nominal tape width for CUPS custom +sizes; this does not permit selecting another cassette or enlarging its physical +printable window. The submitted length and explicit margins remain authoritative. + +CUPS 2.4's generated PPD has one custom-size margin for all tapes. It chooses the +largest advertised margins, so `media=Custom.12x40mm` leaves only about 42 printable +dots on 12 mm tape. A fitting design prints at 1:1; a 76-dot design is rejected. +Named media presets carry the correct per-tape margins. For arbitrary lengths +with the full imageable width, submit standard `media-col` dimensions and the +published tape margins. The server never replaces explicit margins or shrinks +content to conceal this CUPS limitation. ### Cutting -Select cutting through the standard IPP job attribute `finishings`: +One IPP job is one batch: its PNG copies or actual raster labels. Select cutting +through standard `finishings` or `finishings-col`: -| Selection | IPP enum | Behavior | -|-----------|----------|----------| -| `none` | `3` | No leader, intermediate, or terminal cuts; leave the tape run open | -| `trim` | `11` | Cut each requested copy and finalize the last label | +| Selection | IPP selection | Behavior | +|-----------|---------------|----------| +| Chain and continue | `finishings-col` template `ptouch-chain` | Cut between labels; hold the last end | +| Chain and finish | `finishings=60`, or compatibility alias `11` | Cut between labels and cut the final end | +| No label cuts | `finishings=3` | Join labels; hold the batch end | +| Final cut only | `finishings=63` | Join labels; cut only at batch end | -PAPPL also accepts `finishings-col` with `finishing-template=none` or `trim`. -An omitted option uses the advertised `finishings-default`, currently `none`. -Clients that require a cut must request it. For example, with a CUPS queue named -`ptouch`: +**Precut is mandatory once per nonempty batch**, including none and final-only. +A fresh run discards its initial leader. A healthy continuation releases the +preceding held end without a new discarded leader. Consecutive none batches +therefore become separate uncut strips. An unfinished run can remain open across +idle gaps; canceled/rejected followers do not silently finish it. + +Omission honors the current printer default, initially `trim` (11), so ordinary +jobs cut their labels free. Request none explicitly to leave a batch open. +With a CUPS queue named `ptouch`: ```sh -lp -d ptouch -o finishings=3 intermediate.png -lp -d ptouch -o finishings=11 final.png +lp -d ptouch -n 3 -o finishings=60 labels.png # three separate labels, final cut +lp -d ptouch -n 3 -o finishings=63 strip.png # one joined strip, final cut +lp -d ptouch -o finishings=3 unfinished.png # hold its end for continuation ``` -The server does not infer a batch boundary from queued jobs. A no-cut run remains -uncut during idle time or if its intended final job is canceled or never arrives. -A subsequent job continues without resetting the unfinished run, even after an -idle gap. State is kept in memory, so restarting the server during an unfinished -run is not supported. `none` disables all cutting; it does not promise that the -intermediate labels are individually separated. +Registered collection templates `trim`, `trim-after-pages`, and `trim-after-job` +select the corresponding enum policies; collection `no-value` selects none. +`ptouch-chain` is a printer-defined name carried through the standard collection, +with the meaning shown above. Generic dialogs may expose only registered enum +choices; a client must support collection/template submission to select chain-only. +Do not submit both finishing representations in the same job. Enum 60 plus 63 +means chain-and-finish with one terminal cut; enum none alongside other values +is ignored according to IPP. + +Printer defaults accept the three policies with registered enum equivalents; +enum and collection defaults stay synchronized. Chain-only is explicitly per-job, +because there is no lossless registered enum for its printer-defined meaning. +Existing CUPS queues can retain a cached or explicit none default: refresh their +standard queue settings to use trim when adopting this change, for example +`lpoptions -p -o finishings=11`. This was verified with an ordinary +CUPS driverless queue; its generated PPD can otherwise submit explicit none +even to a server advertising trim by default. Explicit none remains honored. Retire old `finishings-col` template `none` submissions in favor +of enum 3 or standard collection no-value. + +The server uses each complete raster label and successful end-of-input, so an +unknown total count requires no custom metadata. PNG copies support 1 through +999; PWG raster and URF support one copy with multiple labels in the document. +Run state lives in memory. Restart during an unfinished run is unsupported; +finish it deliberately first. Interrupted output requires printer power-cycle +followed by server restart, as described below. ## Configuration -Cutting is selected by the job's finishing option. These environment variables -configure the remaining print behavior; the driver reads them per job. - -| Variable | Values | Meaning | -|----------|--------|---------| -| `PTOUCH_PRECUT` | `1` (default) / `0` | Enable leader cutting for `trim` jobs; `none` always disables it | -| `PTOUCH_WIDTH_GUARD` | `strict` (default) | Refuse to print when the rendered width does not match the loaded tape. Only `strict` is implemented; any other value falls back to `strict` | - -`PTOUCH_CUT_MODE` is obsolete and ignored; it cannot override job finishings. +Cutting uses the job's standard finishing choice. `PTOUCH_PRECUT`, +`PTOUCH_CUT_MODE`, and `PTOUCH_WIDTH_GUARD` are retired and ignored. Mandatory +batch precut and strict geometry rejection are intrinsic; no replacement server +knob is needed. `PTOUCH_MODEL` defaults to the supported PT-2730. `PTOUCH_DEVICE_URI` overrides device discovery with an explicit URI (e.g. `socket://127.0.0.1:9100`) for testing without a PT-2730; the printer registers @@ -173,14 +236,28 @@ but only connects on an actual print. The deploy image is published to GHCR as `ghcr.io/pfa230/ptouch-print-server:edge` (the moving tag) and a per-commit `sha-` tag for rollback. It is built and pushed by -`.github/workflows/deploy.yml` on every push to `main`; pull requests build and -run an IPP smoke test but do not push. +`.github/workflows/deploy.yml` on every push to `main`. Both pull requests and +main run the core, driver, and capture suites and submit real IPP jobs to a socket +sink in the deploy-image smoke before publication. Pull requests do not publish. The image is a multi-stage build (`Dockerfile.deploy`): PAPPL 1.4.11 and the app compile in a Debian build stage, and only the runtime libraries, `avahi-daemon`, and `dbus` ship in the `debian-bookworm-slim` runtime stage. The entrypoint starts a system dbus and Avahi, then runs `ptouch-app server` on port 8000. The -container has no persistent state, so no volume is needed. +application writes normal startup, job, and error diagnostics directly to stderr, +visible through `docker logs`; no debug logfile or separate tail process is +needed. dbus and Avahi startup failures stop the entrypoint and remain visible. +The container has no persistent state, so no volume is needed. + +If a job reports uncertain output, inspect `docker logs ptouch` and +`docker exec ptouch ptouch-app jobs -d ptouch` before restarting; job states and +standard IPP Get-Job-Attributes identify aborted work. Power-cycle the printer, +then run `docker restart ptouch` and check +`docker exec ptouch ptouch-app status -d ptouch`. Explicitly resubmit the desired +failed job after recovery. A clean status or USB replug alone does not clear +uncertainty; server restart alone does not clear the printer's buffered raster. +In-memory job history and an unfinished tape run are not promised to survive +restart, and the server does not automatically replay ambiguous output. ```yaml services: @@ -193,9 +270,6 @@ services: nofile: # dbus needs 65536; below that it dies, soft: 65536 # avahi dies with it, and DNS-SD hard: 65536 # advertises nothing at all - environment: - PTOUCH_PRECUT: "1" - PTOUCH_WIDTH_GUARD: strict volumes: - /dev/bus/usb:/dev/bus/usb # BIND MOUNT, not `devices:` - see below device_cgroup_rules: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index 4e6c75b..a58237c 100755 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -5,7 +5,4 @@ mkdir -p /run/dbus /run/avahi-daemon rm -f /run/dbus/pid /run/avahi-daemon/pid dbus-daemon --system --fork avahi-daemon -D # NOTE: -D syslogs; avahi lines may not reach docker logs -# the app logs to /tmp/ptouch.log (system_cb, not stderr); surface it to `docker logs` (Codex #3) -touch /tmp/ptouch.log -tail -F /tmp/ptouch.log & exec ptouch-app server diff --git a/docs/ADDING-A-MODEL.md b/docs/ADDING-A-MODEL.md index c7616ca..f33aa06 100644 --- a/docs/ADDING-A-MODEL.md +++ b/docs/ADDING-A-MODEL.md @@ -1,8 +1,9 @@ # Adding a P-touch model This guide explains how to add a Brother P-touch model to the device table. A new -entry makes the model **recognized and discoverable**. It does **not** make it -print-supported. See the caveats at the end. +entry makes the model **recognized in metadata**. Discovery and printing remain +restricted to the explicitly supported PT-2730. A table row does not grant +print support. See the support gate below. ## The `pt_dev` fields @@ -76,7 +77,9 @@ dispatch lands. 3. Rebuild and run the core tests: ```bash - cmake -S . -B build && cmake --build build && ctest --test-dir build + cmake -S . -B build-core -DPTOUCH_CORE_ONLY=ON + cmake --build build-core + ctest --test-dir build-core --output-on-failure ``` ## Hard rule: do not add wide-head models yet @@ -87,14 +90,10 @@ dispatch lands. The print path is fixed at **16 bytes / 128 dots**. `pt_pack_line` and `pt_pack_window` (`src/raster.h`) pack into a 16-byte line. -Note this is no longer a simple "reject anything wider" check. Since media is -advertised at the nominal tape width with real margins (ADR-0005), `cupsWidth` is -the **full page** width - 170 px for 24 mm tape - and `r_writeline` extracts the -printable window `[left, left + win)` and centres it in the head, clamping `win` -to 128. So a wide-head model would not fail loudly; it would silently print only -the middle 128 dots of its page. Adding one requires generalizing the packing -width, the clamp in `r_writeline`, and the `px > 128` eligibility test in -`media_col_for` together. +The supported-model gate rejects other models before discovery or print output, +including models with wider heads. Supporting one requires a validated protocol, +generalized packing and imageable geometry, and an explicit support-gate change; +do not enable it merely by adding metadata. ## Caveats: recognized is not supported @@ -104,11 +103,12 @@ A new entry is **untested and not print-supported** until: hardware-certified. The status read, media detection, and print geometry are all validated against it and nothing else. - For PackBits or P700 models (`FLAG_RASTER_PACKBITS`, `FLAG_P700_INIT`), per-model - flag dispatch exists in the driver. Today the print callbacks emit PT-2730 - protocol (`FLAG_NONE`) unconditionally, so a flagged model would receive the - wrong byte stream. + flag dispatch exists in the driver. The current supported-model gate rejects + these models before output; it does not send them PT-2730 bytes. -Until then a new row only adds discovery and metadata. +Until then a new row only adds lookup metadata. `pt_usb_supported_model` and the driver identity check must remain +restricted to the implemented, validated print path, and rejected models must +receive no output. ## Source of new PIDs diff --git a/docs/adr/0002-custom-ptouch-usb-scheme.md b/docs/adr/0002-custom-ptouch-usb-scheme.md index 750fdef..8656573 100644 --- a/docs/adr/0002-custom-ptouch-usb-scheme.md +++ b/docs/adr/0002-custom-ptouch-usb-scheme.md @@ -16,15 +16,29 @@ Register a custom `ptouch://` scheme (`papplDeviceAddScheme`) implemented direct create a fresh `libusb_context` per operation rather than holding one for the process lifetime. URIs are model-based (`ptouch://Brother/PT-2730`) and resolved to a device by VID/PID at open time, -so a URI stays valid across replug and bus renumbering. An empty model matches any supported P-touch. +so a URI stays valid across replug and bus renumbering. PT-2730 is the only implemented print path. +Validate `PTOUCH_MODEL` before discovery: omission or an empty value selects PT-2730; unknown or +recognized-but-unsupported values fail startup rather than falling back. Recognized unsupported +devices appear in discovery diagnostics but are never offered as printable devices or opened. + +Count every matching device before discovery reports a candidate and before opening claims one. +Zero means offline; exactly one can be used; multiple PT-2730 units are ambiguous and none is +claimed. Keep the same queue and model URI when absent or ambiguous, so unplugging a duplicate or +replugging the sole supported printer can restore availability without queue replacement. Never +choose by enumeration order, serial number, or a device-ID probe. ## Consequences - No dependence on IEEE-1284 device IDs, so no hang. - Replug works without restarting the process. -- Anything that touches the device must be gated on the scheme: `pt_usb_read_status` casts the PAPPL - device's data pointer to our libusb struct, so calling it on a `socket://` device is undefined - behaviour. This gate is repeated at every call site and must not be forgotten. +- USB access is guarded centrally and at the short-timeout reader. `pt_usb_read_status` takes the + owning printer's URI and rejects a non-supported transport before accessing connection data. + PAPPL has no public accessor for a device connection's URI; callers pass + `papplPrinterGetDeviceURI`, never an inferred URI based on USB presence. Socket test status uses + the driver's explicit test path, and an attached Brother cannot change its transport type. +- Hardware-free adapter tests inject zero, one, or two devices, unsupported models, claim failures, + and a changed node after replug. A real PAPPL socket fixture confirms rejected reads never + inspect network connection data or issue a USB transfer. These tests do not certify hardware. ## Alternatives considered diff --git a/docs/adr/0004-strict-width-guard.md b/docs/adr/0004-strict-width-guard.md index 40f968e..b8e74ff 100644 --- a/docs/adr/0004-strict-width-guard.md +++ b/docs/adr/0004-strict-width-guard.md @@ -1,30 +1,36 @@ -# 4. Strict width guard: reject size mismatches, never scale silently +# 4. Strict tape and PNG content geometry **Status:** Accepted ## Context -A client can request a media width that does not match the tape physically loaded. Printing anyway -produces a mis-sized or clipped label, silently. +A client can select a page width that differs from the physical tape. A matching +page width also does not prove that a centered PNG fits the printable window: +PAPPL can clip source pixels while the old page-width guard accepts the job. ## Decision -Reject. If the job's cross-tape width does not match the loaded tape, fault the job before any raster -is written, with `document-unprintable-error` and a message naming both widths. +Reject page/tape width mismatches before raster, using confirmed loaded media. +Use the shared `pt_tape_geometry` definition for publication and the canonical +printable window. There is no configurable width-recovery policy. Custom media accepts the +advertised ±0.01 mm rounding tolerance around a nominal tape width; the physical +window remains fixed and a different cassette still fails before raster. -`PTOUCH_WIDTH_GUARD` selects the policy; only `strict` is implemented and it is the default. Any -other value logs that it is unimplemented and falls back to strict, so a mismatch is never silently -accepted. +At `print-scaling=none`, preflight the oriented PNG's physical content before +calling PAPPL's renderer. Accept an imageable-width design that fits the page. +Accept a nominal-width canvas only after verifying that every pixel outside the +physical window is white, then remove those blank margins before placement. +Reject oversized cross-tape content and content exceeding an explicit page's +length instead of silently clipping it. Preserve orientation and square DPI; +no DPI means 180 dpi. JPEG keeps PAPPL's standard placement path. ## Consequences -- A wrong-tape job fails visibly instead of wasting tape on an unreadable label. -- The guard must compare like with like. This bit us twice: ADR-0005 changed what `cupsWidth` means, - and the live-tape override in the same function was not updated, so it compared imageable px - against full-page px and faulted correct jobs. -- Clients are expected to preflight with `media-ready`. +Wrong tape and oversized 1:1 PNGs fail visibly without page raster. Explicit +standard scaling remains distinct from a server-selected recovery policy. +Clients can use `media-ready` to design for the actual tape and margins. ## Alternatives considered -- **`fit` mode** (scale or crop to the loaded tape). Deferred as issue #35 and later closed as not - needed: for labels, a visible error beats an unreadable label. +A server-selected fit/crop fallback would change the design's physical size or +lose content without the client's instruction. Keep strict rejection instead. diff --git a/docs/adr/0005-nominal-media-size-with-margins.md b/docs/adr/0005-nominal-media-size-with-margins.md index 054d576..affb2be 100644 --- a/docs/adr/0005-nominal-media-size-with-margins.md +++ b/docs/adr/0005-nominal-media-size-with-margins.md @@ -1,43 +1,46 @@ -# 5. Advertise nominal tape width with real margins +# 5. Nominal tape width and one canonical imageable window **Status:** Accepted (supersedes the original M3 geometry decision) ## Context -The print head is 128 dots wide (~18.07 mm at 180 dpi), narrower than the 24 mm tape it can carry. -The first implementation advertised media at the **printable** width with zero margins, because that -made `cupsWidth` equal the imageable pixel count exactly. - -The consequence surfaced in practice: a 24 mm tape announced itself as `om_18.07x100mm`, and any -client reasoning in tape sizes rejected it ("template requires 24mm media but 18.07mm is loaded"). -That conflates IPP's `media-size` (the physical medium) with the printable area. +The PT-2730's 128-dot head is narrower than 24 mm tape. Advertising only the +printable width as media made 24 mm tape appear as roughly 18 mm media. Integer +conversions of symmetric physical margins also gave the renderer and driver +different printable windows, clipping edge pixels on otherwise valid designs. ## Decision -Advertise the **nominal** tape width as `media-size`, and express the unprintable edge as symmetric -left/right margins: - -``` -size_width = tape_mm * 100 (24 mm -> 2400 centimm) -printable = imageable px in centimm (128 px -> 1807) -left/right = (size_width - printable)/2 (-> 296 each) -``` +Advertise nominal physical width as `media-size`. Use one shared tape geometry +helper at 180 dpi to derive nominal pixel width, the exact imageable pixel count, +an exclusive canonical pixel interval, and publishable physical margins. +Supported widths are 6/9/12/18/24 mm, with 32/52/76/120/128 imageable dots. + +Left and right IPP margins may differ slightly so conversion back to printer +pixels agrees with the canonical interval and PAPPL's image-placement arithmetic. +The printer-wide minimum margin permits every supported tape; each actual +`media-ready` and default carries its own margins. Selected margins cannot expand +the physical printable window. Per-width custom ranges admit only ±0.01 mm +rounding around the nominal width, retaining the submitted dimensions and +physical imageable guard. This accommodates CUPS metric-to-point conversion. +CUPS 2.4 PPDs still apply one global maximum custom margin, so full-width designs +need a named preset or standard media-col with the published tape margins; +explicit client margins are never silently replaced. + +`cupsWidth` remains the full nominal page width. The adapter extracts the +canonical physical window from each full raster line and centers it in the head. +Physical tape comparison therefore uses full nominal page width, while PNG source +preflight uses the physical imageable width. Do not mix these coordinate spaces. ## Consequences -- Media reads as `om_24x100mm`, `om_12x100mm`. Clients match the tape they bought. -- **`cupsWidth` becomes the full media width, on every CUPS version.** `cupsRasterInitHeader` computes - `media->width * xdpi / 2540` and applies margins only to `ImageBox*`. CUPS 2.5 does not change this - (verified in source after an incorrect assumption that it would). -- Therefore the driver must extract the printable window `[left, left+width)` from each raster line - and centre it in the head. This is not a workaround; it is how PWG raster is designed, and it is - what every real printer driver does. -- The width guard now compares full-page px to full-page px (see ADR-0004). -- 36 mm tape stays unadvertised: its imageable width exceeds the head. +Clients see the tape size they bought, including 18 mm ready-media that was +previously rejected by the printer-wide margin bound. One definition serves +publication, validation, and extraction. 36 mm remains unadvertised because its +imageable width exceeds the head. ## Alternatives considered -- **Keep printable-as-media-size** and have clients adapt. Rejected: every client would need the same - workaround, and the declaration would stay wrong. -- **Wait for CUPS 2.5.** Rejected once its raster code was actually read - it changes the API shape, - not the width semantics. +Printable-as-media-size requires every client to compensate for a false physical +size. Relying on a CUPS version change does not correct the geometry: CUPS computes +full media width and represents margins through its image box. diff --git a/docs/adr/0006-own-png-filter.md b/docs/adr/0006-own-png-filter.md index 6ce1aa9..38845de 100644 --- a/docs/adr/0006-own-png-filter.md +++ b/docs/adr/0006-own-png-filter.md @@ -1,39 +1,42 @@ -# 6. Own the `image/png` filter to derive label length from the image +# 6. Standard PNG route with optional image-derived length **Status:** Accepted ## Context -Clients send a PNG and no media length. PAPPL renders it onto a page whose size comes from -`media-size` and, with `print-scaling=none`, centres it. With a fixed 100 mm default media length, a -short label printed with large blank runs before and after it. - -PAPPL does not auto-size pages for continuous media: `cupsHeight` comes straight from the media, and -the image filter only places and clips within it. By the time the driver's raster callbacks run, the -page is fixed and the PNG's own dimensions are gone. +PAPPL places PNGs on selected/default media. Without a concrete page length, +continuous tape labels formerly received the fixed 100 mm default. The first +owned filter corrected that case but overwrote explicit lengths and exposed a +private raster MIME that could never be printed directly. ## Decision -Register our own `image/png` filter and build the page from the submitted image. - -PAPPL's `papplSystemAddMIMEFilter` compares only source and destination MIME types and ignores a -duplicate registration, so `image/png -> image/pwg-raster` cannot be overridden. Instead the driver -declares a private `driver_data.format` and registers `image/png -> `, which job -processing tries first. `validate_driver` requires a `printfile_cb` alongside a non-null `format`, so -one exists and simply faults the job. - -The filter reads the PNG, derives the label length from the image and its resolution (falling back to -180 dpi), rewrites every length-derived header field together, then calls `papplJobFilterImage`, -which continues to drive the existing raster callbacks. +Keep libpng decoding and PAPPL's existing image renderer. Set the driver's format +to `image/png`, register `image/png -> image/png` for the owned filter, and provide +a real PNG `printfile_cb` using the same path. PAPPL selects the matching driver +filter before its built-in PNG-to-PWG filter, and deduplicates the public PNG MIME. +No private destination type or always-failing raw callback is needed. + +Build options after loaded-media confirmation. Preserve submitted named media or +`media-col.media-size.y-dimension`. Only when concrete length is omitted, derive +length from the oriented image and its square DPI metadata, falling back to 180 +dpi. Source-only/margin-only collections do not constitute an explicit size. +Update all length-derived header fields together; use the 5 to 300 mm limits. + +Pin automatic orientation from the source aspect before deriving page length: +tall/square images use portrait and wide images use reverse-landscape (90 degrees +clockwise), while explicit orientation remains authoritative. Decode the complete +PNG, verify 1:1 physical content and blank nominal margins as described in ADR-0004, +and then call `papplJobFilterImage`. Its copy loop expands the job's copies exactly +once and drives the production raster callbacks. ## Consequences -- A label is exactly as long as the submitted image, including deliberate white padding. -- Over-length faults rather than clamping: `papplJobFilterImage` clips centred, so a clamp would - silently eat both ends of the design. -- **Orientation must be pinned from the image.** PAPPL auto-rotates by comparing the image aspect to - the page aspect, which becomes circular once the page length is derived from the image. Real - clients send landscape labels; without pinning they print rotated and clipped. Landscape images - pin `reverse-landscape` (90° CW) so that the start of the design emerges first and prints upright (#44). -- The private MIME type appears in `document-format-supported`. -- Owning this filter later provided the only place able to fix media staleness (ADR-0007). +Explicit 50 mm PNG pages stay 50 mm. Omitted lengths retain the PNG convenience, +including deliberate white padding and minimum-length padding. Overlong or +oversized 1:1 content fails before page output. Standard scaling requests remain +PAPPL's responsibility. JPEG keeps its standard selected/default media sizing; +this decision adds no JPEG decoder or automatic length promise. + +Real IPP tests verify MIME routing, copy counts, named and collection geometry, +asymmetric orientation markers, physical margins, and length boundaries. diff --git a/docs/adr/0007-offline-is-a-state.md b/docs/adr/0007-offline-is-a-state.md index 72dcaa4..66be373 100644 --- a/docs/adr/0007-offline-is-a-state.md +++ b/docs/adr/0007-offline-is-a-state.md @@ -14,7 +14,9 @@ was no printer object to attach reasons to. ## Decision Always create the printer. When no device is present at startup, create it on -`ptouch://Brother/${PTOUCH_MODEL:-PT-2730}` and mark it `offline` immediately. +`ptouch://Brother/PT-2730` and mark it `offline` immediately. Validate configuration +first: PT-2730 is the only implemented model, and an unsupported value cannot select +a fallback device. Jobs submitted while offline are accepted and queued; PAPPL pauses the queue and retries the device. When the printer appears, the queue resumes with no restart. @@ -29,3 +31,18 @@ When the printer appears, the queue resumes with no restart. already exists there and can only be faulted, not repaired. That refresh is a barrier: if the tape cannot be read it faults explicitly rather than building a wrong page. - The device node must be a **bind mount**, not compose `devices:` - see ADR-0009. +- USB presence does not establish readiness. An unclaimable device, incomplete or + malformed status, unsupported cassette, or printer error leaves the queue + unavailable. Idle polling and job-owned refresh share the same publication path. +- Each queue owns its run and media-confirmation state through PAPPL driver + extension data. Status queries do not initialize the printer or finalize an + unfinished tape run. +- An interrupted raster/print command can leave output uncertain. Block later work + and report recovery required, even if a later status reply is clean or USB + reopens. The conservative recovery sequence is to power-cycle the printer, + then restart the server and inspect failed jobs before explicit resubmission. + Do not automatically reset, cut, replay, or append to the uncertain buffer. + Server restart alone does not establish a safe physical tape state, and the + container's process-local queue/state is not promised to survive it. The authorized + integrated PT-2730 acceptance verified blocking after truncated raster and + successful power-cycle/server-restart recovery without replay or ghost output. diff --git a/docs/adr/0008-cross-job-chaining.md b/docs/adr/0008-cross-job-chaining.md index 71641c1..eecf6d3 100644 --- a/docs/adr/0008-cross-job-chaining.md +++ b/docs/adr/0008-cross-job-chaining.md @@ -1,52 +1,81 @@ -# 8. Respect client finishing and never reset an unfinished run +# 8. Job-defined batches with explicit finishing and safe continuation -**Status:** Accepted; revised on 2026-10-11 to replace queue inference. +**Status:** Accepted; revised by simplify-print-server. ## Context -Different PNG labels arrive as separate IPP jobs. The old driver checked the -queue before the final label started, which missed followers arriving during -printing. It also cut an open run from an idle timer. Neither signal describes -what the client wants. +Queue contents and idle time do not describe whether a client wants a batch +finished. Earlier queue-drain finalization could miss jobs arriving during +printing. FF followed by an empty EJECT also failed on the PT-2730. -The driver already advertises trimming through IPP. PAPPL resolves both -`finishings` and `finishings-col`, including defaults, into its job options; -the driver previously ignored that value in favor of `PTOUCH_CUT_MODE`. - -Earlier hardware investigation established that sending `ESC @` into an open -run could wedge the PT-2730 with a cassette-changed message. Suppressing that -reset remains necessary regardless of who decides whether to cut. The attempted -queue-drain sequence of FF followed by a standalone EJECT failed its hardware -probe and is not used by this decision. +The former copy-based planner treated copies as labels, failed multi-label +raster with one copy, and imposed a 64-copy ceiling. PAPPL's legacy finishing +bitmask also loses registered 60/63 and the chain-only collection selection. +Sending ESC @ into a held run can wedge the printer, so preserving healthy +continuation remains necessary. ## Decision -Use only the resolved job finishing choice to select the existing cutter plan: -`none` disables all cuts and leaves the run open; `trim` cuts copies individually -and finalizes the final label with chain-off/EJECT. An omitted choice honors the -advertised default, currently `none`. `PTOUCH_CUT_MODE` is ignored. - -Remove queue scans, page-start replanning, the idle cutter timer, and automatic -terminal output during abort cleanup. Keep per-printer open-run and media state -to suppress `ESC @` between unfinished jobs, including after idle time and a -handle close/reopen. Retain rasterstart. Idle status polling must not reset an -open run: request status without `ESC @`, including before the next PNG job's -geometry is built, so cassette changes are still detected. The existing USB -presence check still detects disconnection. - -Check actual write/flush transfers before recording terminal output, without -replaying a failed label or cut. At USB page boundaries, consume unsolicited -status until receiving-ready before sending another page. A print-completed -packet can precede that readiness indication; waiting for it alone leaves a -stale packet for the following page. This is device flow control, independent -of the finishing decision. +One IPP job defines one batch. Expand PNG copies once; process actual raster +labels without requiring a declared total. Resolve original standard attributes +and the current synchronized printer default using one driver preset table: + +| Preset | Between-label cuts | Final cut | Standard IPP selection | +|--------|--------------------|-----------|------------------------| +| Chain-only | Yes | No | finishings-col template ptouch-chain | +| Chain-and-final | Yes | Yes | 60; compatibility alias 11 | +| No label cuts | No | No | 3; collection no-value | +| Final-only | No | Yes | 63 | + +The implementation-defined name ptouch-chain uses the standard finishing +collection; it adds no independent vendor attribute or private enum. It remains +per-job, because it cannot be represented losslessly as an enum printer default. +The initial canonical default is trim (11). Enum and collection updates to +representable defaults must stay synchronized; conflicting updates are rejected. + +Precut prepares the first actual label of every nonempty batch, including none. +Later autocut depends only on between-label policy. The next batch releases the +held prior end without a discarded leader. Thus none/none creates separate +joined strips rather than one permanently joined multi-job strip. + +Keep constant-size job state with two policy flags, actual-label progress, +one pending print command, and a failure latch. A next valid label releases the +prior complete label with FF and waits for receiving-ready. Successful batch end +releases the last label with FF for continuation, or late chain-off/EJECT when +finishing. Hold this decision until actual successful end-of-input; do not fully +feed and eject each label or buffer another raster image. + +The queue owns closed/healthy-open/uncertain state and media confirmation. +Reset only a known closed run. Idle/job status queries confirm cassette and +errors without initialization or finalization. A canceled or rejected follower +that sent no page output preserves the previous healthy run. Ambiguous transfer, +decode, or readiness failures block further output until printer power-cycle +followed by server restart. Cleanup emits no pending feed, cut, or replay and +is idempotent. + +Bounded pinned PAPPL corrections allow advertised enums/templates, original job +attributes, and synchronized defaults through the ordinary IPP boundary. The +CUPS/PAPPL raster correction distinguishes malformed termination from EOF. +Neither patch introduces a raster parser, spooler, or generic finishing engine. +See patches/README.md for causes, version/security policy, and removal criteria. ## Consequences -- Client intent controls the cut even if other work is queued or the queue is empty. -- Clients must request `trim` when they want cutting; the advertised none default is honored. -- No-cut jobs remain uncut until an explicit trim job or physical intervention. -- `none` disables every cut and can leave a strip of joined labels. -- PNG geometry, existing formats, width checks, copy limits, and scheduling stay unchanged. -- Process restart does not preserve unfinished-run state. -- Socket captures establish command behavior; PT-2730 output must separately verify cuts and leaders before claiming the issue fixed. +Default printing now cuts labels free; clients wanting continuation request it +explicitly. Cached CUPS defaults must be refreshed through standard queue +settings. Retire optional precut and old cutter environment settings. PNG copies +support 1–999 while streaming raster advertises one copy with actual multi-label +documents. Generic dialogs may not expose the chain-only template. + +Run state is process-local, and unfinished-run restart is unsupported. Complete +a healthy run deliberately before restart; recover uncertain output explicitly +without promising queue persistence or automatic resubmission. + +The eight-label direct-protocol PT-2730 gate on 24 mm tape passed all four modes, +mandatory precut, late finalization, and continuation without discarded leaders. +The separately authorized 21-label integrated acceptance on 12 mm tape also +passed PNG copies, unknown-count raster, idle/offline continuation, cassette +change, uncertain-output blocking, and explicit recovery. The operator confirmed +rectangles, different label lengths, orientation, and no ghost output. These +results certify the tested PT-2730 cases; 6/9/18 mm retain software-only coverage. +See tests/HARDWARE-ACCEPTANCE.md and the change's validation.md. diff --git a/docs/adr/README.md b/docs/adr/README.md index 2ae9f57..3476779 100644 --- a/docs/adr/README.md +++ b/docs/adr/README.md @@ -8,10 +8,10 @@ record a mistake and its correction, because the correction is the useful part. | [0001](0001-own-pappl-application.md) | Build our own PAPPL application rather than extending LPrint | | [0002](0002-custom-ptouch-usb-scheme.md) | Custom `ptouch://` USB scheme over libusb, per-operation contexts | | [0003](0003-pure-core-separate-from-adapter.md) | Pure C core, unit-testable without hardware | -| [0004](0004-strict-width-guard.md) | Strict width guard: reject mismatches, never scale silently | +| [0004](0004-strict-width-guard.md) | Strict tape and PNG content geometry | | [0005](0005-nominal-media-size-with-margins.md) | Advertise nominal tape width with real margins | -| [0006](0006-own-png-filter.md) | Own the `image/png` filter to derive label length from the image | +| [0006](0006-own-png-filter.md) | Standard PNG route with image-derived length when media length is omitted | | [0007](0007-offline-is-a-state.md) | Offline is a printer state, not an absent printer | -| [0008](0008-cross-job-chaining.md) | Respect client finishing; never reset an unfinished run | +| [0008](0008-cross-job-chaining.md) | Four batch finishing presets and efficient continuation | | [0009](0009-deployment-topology.md) | Container USB hotplug and reverse-proxy requirements | | [0010](0010-gpl-licensing.md) | GPL-3.0-only, forced by derived protocol code | diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/.openspec.yaml b/openspec/changes/archive/2026-10-13-simplify-print-server/.openspec.yaml new file mode 100644 index 0000000..b9ebfc7 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-12 diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/design.md b/openspec/changes/archive/2026-10-13-simplify-print-server/design.md new file mode 100644 index 0000000..26f12d4 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/design.md @@ -0,0 +1,160 @@ +# Design + +## Context + +See proposal.md for motivation and the three delta specifications for behavior. This design implements the audit as four related areas rather than nine independent fixes. + +[verified] The audit's temporary socket/IPP probes reproduced a two-page PWG job ending aborted, a requested 50 mm PNG printing 25.4 mm, a 200-pixel-wide PNG losing both edge markers, and PAPPL rejecting 18 mm ready-media. Existing six container suites and seven capture tests passed, so those results do not cover the missing contracts. + +[verified] The adapter already has useful protocol builders, checked USB writes, receive-ready waits, nominal tape dimensions, and no-reset handling for unfinished tape. Keep these. PAPPL owns IPP, discovery, scheduling, image placement, and raster conversion; the adapter owns the Brother device and label-specific decisions. + +## Goals / Non-Goals + +**Goals:** Constant-size lifecycle state; actual-label processing with explicit batch boundaries; four finishing presets and mandatory precut; no discarded leaders between healthy continued batches; truthful IPP capabilities; fewer production workarounds and operational processes. + +**Non-Goals:** New public protocols or independent vendor job attributes, a label designer, new Brother model support, a general printer framework, multi-printer identity provisioning, persistent batch state, a new raster renderer, or automated production rollout. Keep unfinished-run restart limitations documented. Do not add queue inference, idle finalization, or automatic content retries. + +## Decisions + +### 1. Four finishing presets share one efficient tape run + +One IPP job is one batch: all copies of its PNG or all actual labels in its raster document. Successive jobs remain separate batches even when they continue the same physical tape run. Internally the policy has two facts, `cut_between` and `cut_at_end`: + +| Preset | Between labels | Final batch cut | Public IPP selection | +| --- | --- | --- | --- | +| Chain and continue | Yes | No | `finishings-col` template `ptouch-chain` | +| Chain and finish | Yes | Yes | `trim-after-pages` (60), with `trim` (11) as compatibility alias | +| No label cuts and continue | No | No | `none` (3) | +| Final cut only | No | Yes | `trim-after-job` (63) | + +Initialize enum/collection defaults to the existing `trim` alias for chain-and-finish. Omission honors the current advertised default. The two internal facts are not exposed as independent public switches: registered 60 already includes the last label, so combining 60 and 63 remains chain-and-finish and must not emit two terminal cuts. + +Precut is mandatory batch preparation, not another setting. Enable first-label autocut for every nonempty batch. Enable later-label autocut only when `cut_between` is true. A fresh run cuts its initial leader; on a healthy open run the next batch's precut must separate the previous held end with no discarded leader between batches. Consequently two no-label-cuts batches become two strips, separated by the second batch's precut. This explicitly revises the old joined none/none contract. + +Keep chaining between labels. Final feed/cut happens only at successful batch end when requested; do not fully finalize each label. Remove `PTOUCH_PRECUT` from the supported interface, alongside the already-obsolete cut-mode and ineffective width-policy settings. Reject unsupported/conflicting finishing choices before output and never use queue length or elapsed time to select a policy. + +### 2. Advertised IPP presets survive validation and decoding + +[verified] [IPP Finishings 3.0](https://ftp.pwg.org/pub/pwg/candidates/cs-ippfinishings30-20220527-5100.1.pdf), section 5.1.5, assigns 60 to trimming after pages and 63 to trimming after the job. Section 5.2.5 allows implementation-defined finishing-template names. `ptouch-chain` uses that standard collection mechanism with a printer-defined meaning; it is not a registered portable chain flag. Some generic dialogs will only expose registered choices, so prove collection submission through CUPS and document the discoverable template. + +[verified] Pinned PAPPL validates and resolves finishings through fixed none/punch/staple/trim bits. Merely adding advertised values will not work: `printer-ipp.c` rejects unsupported bit mappings, and `job-process.c` cannot preserve 60, 63, or the named chain-only choice in `options->finishings`. + +Use one small driver preset table for advertised enums/templates, aliases, defaults, and resolution to the two internal facts. Resolve the original job attributes using `papplJobGetAttribute` and the advertised defaults using `papplPrinterGetDriverAttributes`; do not depend on the lossy bitmask. Extend the pinned PAPPL boundary so the driver's advertised finishing values are accepted, capability overrides are unambiguous, and default updates remain consistent. Keep ordinary handling for other drivers unchanged. Reject unknown values and conflicting presets even when a client permits attribute substitution; unsupported finishing must not quietly become no-cut. + +[verified] FIN section 6.10 requires enum and collection defaults to describe the same finishing processes; for enum none the collection default must be the out-of-band `no-value`. Keep one canonical default preset, limited to the three presets with enum equivalents. An enum or equivalent collection default update changes that preset and republishes both representations together; reject contradictory simultaneous updates without changing either. Reject ptouch-chain as a printer default because there is no lossless registered enum equivalent. It remains available explicitly per job, including a client queue preference that submits the template with each job. Omitted job selections use the canonical default, while explicit enum or collection requests override it. Add no fallback enum or hidden precedence rule. + +Advertise matching supported/ready/template/database values and useful labels for the choices. Use collection `no-value` for the no-label-cuts choice rather than inventing a registered none template. Accept supported templates and registered combinations with equivalent meaning, including 60 plus 63. FIN section 5.1 requires ignoring enum none when accompanied by other finishing values; normalize it before resolving the remaining selection. Reject both enum and collection attributes in the same job request. Test requests and defaults through real IPP; do not treat parsing a private option in the driver as equivalent to library acceptance. + +Alternative considered: Claude's proposed `ptouch-tape-run=finish|continue` vendor attribute. It makes the two facts independent at the cost of a new public contract and changes the meaning of existing none submissions. Do not add it. Do not reinterpret 60 as all-except-last or assign an unregistered numeric enum to chain-only. + +### 3. Small owned state replaces operation arrays and global slots + +Allocate one printer state object in `pappl_pr_driver_data_t.extension`, free it in `delete_cb`, and retain its mutex for status/job concurrency. Track closed, healthy open, or uncertain run state plus media confirmation; do not introduce a general registry. The application continues creating one queue and retains PAPPL's ordinary web interface. + +[verified] Retain `PAPPL_SOPTIONS_MULTI_QUEUE`: pinned PAPPL also uses that flag to select the queue resource and permit multiple pending jobs. Removing it reduces active-job capacity to one and would break existing offline queuing. It is a framework setting with necessary behavior, not a reason to keep a global driver-state registry. Preserve `/ipp/print/ptouch` and test two or more queued jobs while offline. + +Keep job state for the two policy facts, first-label/progress tracking, tape geometry, one pending print command, and a latched failure. The first-label fact comes from this state, not the callback page number: [verified] PAPPL's image copy loop passes page number zero for every copy (`job-filter.c:362`). Otherwise mandatory precut would accidentally cut every copy in a final-only batch. + +Proposed lifecycle: + +1. Start the job only after policy and clean media validation. Reset only a known closed run, never a healthy open or uncertain run. Uncertain state blocks page output pending explicit recovery. +2. At label start, validate its geometry. If the preceding complete label has a pending print command, send FF and consume receive-ready before starting the next label. Configure first-label autocut, or later-label autocut only for cut-between, and chain-on before raster. +3. Write checked raster; latch every failure and stop later writes after it. Mark printer state uncertain while a label is being transferred or its output is unconfirmed. +4. At label end, retain a pending print command for the complete label. Do not send FF/EJECT yet. Receiving the next valid label proves that this one is intermediate; normal job end proves that it is the final one. No declared total label count or application raster buffer is required. +5. At successful job end, send FF for a continuation preset, or late chain-off plus EJECT for a finishing preset, and consume receive-ready. Set healthy open or closed only after confirming the transfer and readiness. Normal job completion must follow successful device output. +6. On cancellation, decode/transfer/status failure, or abort cleanup, send no pending print/feed or final-cut command. Preserve uncertain state when output is ambiguous, free job state, and make repeated cleanup idempotent. Latch failures on every callback exit; checking only job-state is insufficient because PAPPL's image abort path can call rendjob before marking the job aborted. + +[verified] Streaming PAPPL calls rendpage before trying the next header, so it cannot reveal the true final label there. Both its raster and image paths check normal rendjob failure; the image abort path can call rendjob again. The proposed deferred command must distinguish these paths and never replay a terminal attempt. + +[verified] The approved eight-label PT-2730 protocol gate passed on 24 mm tape: chain-only then chain-and-final, and none then final-only, with two labels per batch. The operator confirmed mandatory initial precut, late finalization of multi-label batches, final-only cutting with autocut disabled on later labels, and no discarded leaders during healthy continuation. See validation.md for captures and observations. This permits local production-lifecycle implementation; integrated PNG/raster and recovery acceptance remains pending. Retain other counts in software captures and authorize the larger physical acceptance matrix separately. A failed candidate is grounds to revise its implementation, not to silently drop a required mode, require custom count metadata, or weaken unknown-count support. + +Remove `pt_batch`, the operation array, scans, and the planner-derived 64-copy ceiling. Retire the array representation while preserving the now-required end-only and cross-batch semantics in a small pure policy helper. Existing `PT_CUT_END` is not reusable unchanged: unconditional `do_precut` currently enables autocut on every label. Test observable policy rather than the retired representation. + +### 4. One tape-geometry definition serves publication and validation + +Use the existing supported tape table to derive nominal width, exact imageable pixel width, canonical pixel window, and physical margins. The printer-wide minimum margin must permit every advertised tape, including 18 mm; actual media-ready/default carry the loaded tape's margins. Validate client-selected margins against the physical imageable window so a minimum advertised margin cannot expand what the hardware can print. + +Check page width against confirmed nominal tape width, and source content against the canonical imageable window. Keep bounded rounding conversions in one geometry helper instead of scattering full-page/imageable conversions and correction comments. Validate all five advertised widths in tests. Treat publication failure as a real readiness failure, never silently retain placeholder media as known good. + +For PNG jobs at print-scaling none, accept designs that fit the imageable area. Also accept nominal-width canvases with blank edge margins, extracting only verified blank margins before placement when needed; reject nonblank content outside the physical window. Reject excessive along-tape PNG content when explicit media length is shorter. Keep explicit orientation and the current documented automatic PNG orientation; test asymmetric markers so flips cannot hide in symmetric images. JPEG continues through PAPPL's standard placement path; this change does not promise PNG source-content preflight for JPEG or add another decoder to implement it. + +This change adds no server-selected fit/crop policy. Existing explicitly requested standard scaling must either be honored as advertised or rejected visibly; do not reinterpret it as a silent fallback. The no-content-loss regressions specifically exercise print-scaling none. + +### 5. PNG length is a fallback, and its public format is really PNG + +Keep the working libpng decode and `papplJobFilterImage` path. Identify concrete submitted media length from `papplJobGetAttribute`: a resolved named `media`, or `media-col.media-size.y-dimension`. A source-only/margin-only collection is not an explicit page size. Build options after live tape confirmation. Preserve explicit size; otherwise derive length from the oriented PNG at its DPI, retaining the 180 dpi fallback and 5 to 300 mm range. + +[verified, static API inspection] A minimal supported MIME route exists in pinned PAPPL: set driver format to `image/png`, register `image/png -> image/png` to the owned callback, and provide a real PNG `printfile_cb` wrapper. PAPPL accepts that distinct source/destination pair, selects it before its built-in PNG-to-PWG path, and deduplicates PNG in the public format list. Verify this with an actual IPP upload before removing the current private MIME route. PNG remains a normal input, and neither a private format nor an always-rejecting raw callback is needed. + +[verified] The file-processing path checks the matching filter before raw printfile handling (`job-process.c:534-544`). `papplJobFilterImage` loops over copies and calls normal rendjob after the loop (`job-filter.c:356-573`), so one PNG job remains one batch, rather than copies becoming separate batches or being multiplied twice. Retain an IPP routing/copy test for this boundary and abort-path cleanup. + +Do not rewrite PNG decoding just to reduce its line count. Focus on removing duplicate policy and incompatible geometry behavior. JPEG keeps its existing standard media-based sizing; automatic image length is a documented PNG convenience, not a promise for every format. Explicit geometry has the same meaning across formats. + +### 6. Bound dependency changes to raster correctness and finishing acceptance + +[verified] In PAPPL 1.4.11 `pappl/job-process.c`, incoming raster geometry is copied to callback options only when input and output are both at least 8 bits per pixel. This adapter forces 1-bit output, so callbacks receive default-derived geometry instead. Raster submissions bypass MIME filters. Moving checks into rstartpage or changing the PNG filter cannot fix that boundary. + +Keep documented patches under `patches/` against pinned PAPPL. The raster correction retains incoming per-label dimensions, resolution, and coherent page/image-box geometry in callback options while preserving PAPPL's selected output pixel format and conversion. Recompute output bytes-per-line and allocation sizes from that geometry; make loop bounds follow the actual label so no default-length padding/truncation occurs. Validate geometry before output, including contradictory explicit job/page dimensions. + +[verified] The partial-next-header regression still completed after adding one byte to a valid URF stream: CUPS 2.4.x returns the same read-header result for clean EOF and a truncated next header, without a distinct error. Its compressed input buffer can read ahead, so PAPPL-side HTTP byte counters cannot resolve this safely. The user approved extending dependency scope to a bounded CUPS decoder correction. Report partial-header failure through the existing CUPS error mechanism and propagate it in PAPPL before normal end-job, retaining valid unknown-count streams. Pin a compatible CUPS source with the packaged security corrections; build and ship the corrected shared libraries consistently in both images. Do not add a private decoder, byte-at-a-time HTTP workaround, or unrelated CUPS changes. + +[verified] The existing raster discard loop advances its row counter even when reads fail (`job-process.c:777-784`), hiding truncated input. Require complete raster rows and declared page data, distinguish decoding errors from normal EOF, and mark abort before normal label completion on malformed input. Check scanline callback failures immediately instead of ignoring them. Retain the driver failure latch for repeated cleanup and image-path failures; no pending label may be printed/finalized after a malformed stream or failed callback. + +The finishing correction implements Decision 2: accurate driver capability overrides, validation against advertised enums/templates, and preservation of chosen/defaulted preset semantics. The patch scope is explicitly broader than geometry-only, but bounded to these demonstrated PAPPL integration defects. Keep policy in the driver and add no generic finishing engine. + +Apply identical PAPPL/CUPS patches during Dockerfile and Dockerfile.deploy builds, failing when they no longer apply, and ensure the runtime links the corrected CUPS libraries. Test 1-bit and 8-bit PWG/URF, different lengths in one job, unknown total counts, malformed dimensions, short rows, bad/truncated stream termination, all four finishing choices, default changes, and output failures through IPP rather than only calling callbacks. Keep upstream attribution and removal conditions beside the patches. Remove a patch only when the pinned dependency supplies equivalent verified behavior. + +Alternative considered: a private raster parser or page-buffering pipeline. It would duplicate PAPPL and add more long-term maintenance than the bounded dependency corrections. A broad dependency upgrade is outside this change; no upgrade is assumed to solve the missing callback geometry. + +### 7. Device support, readiness, and uncertain output have one safe boundary + +Validate the configured model before discovery, whether hardware is attached or absent. Default to PT-2730; known-but-unimplemented models must never become printable. Count all matching supported USB devices during discovery and opening. Zero matches means offline; one is usable; more than one means ambiguous and no device is claimed. Preserve fresh libusb contexts, bounded I/O, VID/PID identity, and hotplug-safe model URIs. Do not add descriptor/serial probes that could reintroduce the known device-ID hang. + +Gate USB-specific connection access centrally and also at the low-level reader so arbitrary PAPPL connection data cannot be cast as a USB handle. Keep the socket override as a test facility with a defined non-USB status path; production USB presence must not change its transport type. + +Use one interpretation/publication path for parsed tape and errors, shared by idle polling and job-owned refresh. Distinguish absence, inability to open/claim, missing tape, unreadable/unsupported media, and printer error bits. Publish appropriate existing standard IPP reasons plus useful diagnostics. Confirm clean live status before page raster, including after an offline interval; transient malformed reads must not turn unknown state into ready state. + +Keep the stable offline queue and PAPPL scheduling. A job whose content cannot be safely retried fails visibly; physical content is never automatically replayed during cleanup. + +Uncertain output is a separate readiness condition, not an ordinary healthy open run. Conservatively block new page setup/raster and report recovery required. The documented recovery is to power-cycle the printer, then restart the server, and resubmit explicitly as needed; existing process-local queues are not promised to survive that restart. Do not clear this latch on a clean status reply or USB reopen alone, and do not adopt Claude's automatic ESC @ recommendation: ADR-0008 records that resetting an unfinished run can wedge the PT-2730. Rejection before any page output leaves a prior healthy run intact. Hardware probes must confirm recovery clears buffered content without ghost labels or replay. + +### 8. Build, logging, and delivery remain small + +Use PAPPL log filename `-` (its documented stderr target), INFO for normal operation, and its existing diagnostic controls for debugging. Remove `/tmp/ptouch.log` and the tail process. Keep the dbus/Avahi dependencies required by this deployment; adding a general process supervisor is outside this simplification. Their startup failures must remain explicit in diagnostics. + +Remove the optional precut and ineffective width-policy knobs from the supported interface and example deployment; mandatory first-label precut and strict geometry validation are ordinary behavior. Document retirement with the already-obsolete cut-mode setting. Add an explicit CMake core-only option, preserving hardware-free builds while making a requested/full app build fail when dependencies are missing. + +CI must run the complete app/core tests before pushing an image on main, as well as on PRs. Keep the existing IPP smoke as a deployment wiring check and extend it with a real socket capture submission; it is not physical certification. Ensure the capture tests run too, rather than being silently omitted when Python is unavailable. Use existing test tools instead of adding another test framework. + +## Risks / Trade-offs + +- Default trim changes output for existing clients that omitted finishings. Migration documentation must say to request none explicitly for continuation; expose no hidden compatibility toggle. +- Deferred last-label printing and late finalization change a hardware sequence. Gate production use on all four presets, especially final-only and no discarded leaders between healthy continued batches. A fresh run after deliberate finalization may need its own leader; do not demand one leader across two separately finished runs. Socket bytes alone cannot certify these outcomes. +- Mandatory precut changes none: it still has no internal/final cuts, but the next batch can separate the preceding strip. Document this migration explicitly and remove optional-precut compatibility toggles. +- Dependency patches add maintenance. Bound them to demonstrated raster and finishing integration defects, apply reproducibly, and test real IPP acceptance as well as output; do not grow a separate PAPPL fork. +- The chain-only template has printer-specific meaning. Preserve standard enum semantics, advertise/localize the template, and prove CUPS collection submission rather than assuming every generic dialog can expose it. +- Unfinished/uncertain runs remain process-local and shared within the queue. Explicit recovery may discard unprinted buffered content and requires a printer power-cycle plus server restart; report failures and resubmission needs honestly, adding no persistence or automatic replay subsystem. +- Automatic PNG orientation and physical rounding can move edge content. Test asymmetric portrait/landscape markers, all tape widths in software, and physically verify the tape widths actually available. +- Error mapping must use supported IPP reasons. Preserve raw error information in diagnostics where device-specific meanings are unverified; do not invent a ready state to avoid uncertainty. + +## Migration Plan + +1. Implement locally in the ordered tasks, beginning with regression coverage and bounded protocol probes before committing to the production deferred-finalization sequence. Do not alter the running printer configuration as part of implementation. +2. Run full container/core/capture and CUPS integration validation against an isolated sink; record software evidence separately from physical evidence. +3. With an instruction authorizing physical validation, run the PT-2730 gates for late finalization and final-only cutting, then the integrated four-preset/continued-batch/recovery matrix. Revise an unsuccessful candidate before claiming the required presets work; do not silently drop a mode or replace unknown-count streams with a private count requirement. Leave unavailable tape-width certification explicit. +4. Update README and supersede ADR details for the four presets, mandatory precut, batch lifecycle, geometry, MIME routing, state ownership/recovery, and logging. Review compatibility changes and verify all capabilities. + Document that existing CUPS queues may carry a cached or explicitly configured none default; refresh their standard queue defaults when adopting trim. The server must still honor an explicit none sent by those clients. +5. Sync the delta specs, validate, and archive only when implementation and required validation are complete. Keep commits/branches local until then. +6. Publishing or changing the deployed service requires a separate instruction. For an authorized rollout, retain the previous immutable image and quiesce any explicit unfinished no-cut run before restart; rollback restores that image, not ambiguous physical tape state. + +## Review corrections + +Concrete collection media-size-name participates in image length precedence and +raster contradiction checks just like its resolved dimensions. Idle status skips +active printing and rechecks a busy OpenDevice failure before publishing offline. +Custom width ranges admit only ±0.01 mm around each nominal tape width, retaining +submitted geometry and the unchanged physical printable window. CUPS 2.4's PPD +represents all custom tape widths with one maximum margin: preserve those explicit +margins and reject oversized content; do not invent a client protocol, override +margins, or resize content. Correct per-width margins remain available through +named presets and standard media-col submissions. diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/README.md b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/README.md new file mode 100644 index 0000000..459a6fb --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/README.md @@ -0,0 +1,44 @@ +# Retained validation evidence + +`ptouch-integrated-12mm.tar.gz` contains the authorized hardware session's +IPP requests, generated label fixtures, output offsets, transparent USB capture, +status packets, candidate logs, and original-service restoration records. +Compiled helper binaries and the container image are excluded. `usb-capture.c` +is the transparent bulk-transfer recorder used for that session. + +The four `gate-24mm-*.prn` files are the earlier bounded direct-protocol +captures. Operator observations for both sessions are recorded in ../validation.md; +command captures alone are not proof of physical cuts. + +`claude-implementation-review.txt` is the independent pre-fix review. +`named-raster-*` and `driver-png-*` retain failing reproductions and passing +regressions. `cups-review-fixes.txt` records a normal isolated driverless queue, +including fitting rounded Custom sizes, oversized rejection, PDF copies, and +exact standard media-col dimensions. `final-deploy.txt` and `final-smoke.txt` record the successful final runtime +build, all eleven CTest suites, and real IPP printing into an isolated sink. + +## SHA-256 + +``` +6976a9ad2173cd2384553dbbc796e9cd2d691e804b24d07a6c2614a2dcc1bb26 claude-implementation-review.txt +c2a12c6afb48d6845e50159b94549a68930d2b2cbc2f2a588fc1d7bdf6cf29f2 cups-review-fixes.txt +0ba921089301497c71781f0c93d1b883ee1e77810869f33f0ccd4da8f68db8fe driver-png-before-driver.txt +a255b89275d5ffbfdd6fe02fe3eda0bf01cffbfc9d9bd38d43a20a90cd8fd187 driver-png-before.txt +d878c2f1b4d7b133d4ccb1cb2f5fc00662205b14b6f00e90c9ec54e816b54446 driver-png-final.txt +71443e9397730f2449eb690db02661cf2706fcde4052ef076141c725277dbd0d driver-png-report.md +6182ee24a6399f3c346ef639f17fd19feec0612b2081a70d95c1d8f5c1ad56fd final-deploy.txt +2fb62cf7c6539397952c24c3a3835d851e041c14d4334a280ea46344d4c31e31 final-openspec.txt +8e0907ac2e06ce6d980929a7bd976b292b614324f061e841c9a889ef089e802e final-smoke.txt +f1dda2d3b836a41244c07bf179d8d4d6e24d3fc5af3125358b438756a79c985b gate-24mm-chain-2.prn +e0a02dbc762f1f5ed4e78d945d0d73f31df4361d3aea275c0011a8a3f3a43be5 gate-24mm-each-2.prn +d20e3679e6f50a5df46a444908f9aef688f9f53dda1792485b91b9bca8e2ac70 gate-24mm-end-2.prn +2fc856162c8de5df5e7d29b64f0c78a1ddacf751bb9fc972a875ef8bdf39ed3a gate-24mm-none-2.prn +2ee229a298a9899b409d4dd4a4d13b6016012646d2861fb65dc51458a7e55919 named-raster-after.txt +9d87b6f454ab216a2901e25823e1837fb278b4e237c812377e79d3735667e270 named-raster-bad-context.txt +53dfac86f7aeb46fc64a3ee3b44beb4deb2fc31daa8ddbf9029260341c75e9c9 named-raster-before.txt +2fa53e31ed148030bd15849808f0fb37f2622bda08baa69b25792807f44a1260 named-raster-clean.txt +fc6f3896959cd7c55b9a5f8996e7cd922238acbec52f51c5c389e647af66d5fb named-raster-duplicate.txt +63f8ab34e450f6369310561dc2b13344cc47b472e9272b9aa1e1616c91f385af named-raster-finishings-followup.txt +c40cc79aeb33f1ac458e1ec86ceed30adb33f991327d4b96e3b61b49ffef8488 ptouch-integrated-12mm.tar.gz +584aa8ecb351c08f15ed67169c186fb69db6eece4713c5c2d1ca914f6ccc8c31 usb-capture.c +``` diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/claude-implementation-review.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/claude-implementation-review.txt new file mode 100644 index 0000000..edaf678 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/claude-implementation-review.txt @@ -0,0 +1,55 @@ +I found one verified correctness defect in the implementation. It is medium severity and has a two-line fix. I found no material regressions in the cut lifecycle, failure cleanup and uncertain-output latch, USB selection, the CUPS decoder patch, or the default bridge. Two things remain unverified ordinary-CUPS cases that the tests skip, and some docs are stale and should be fixed before archive. + +## Verified defects + +**1. Medium: a size given only by name inside `media-col` is not treated as an explicit size. PNG then replaces it with the image-derived length.** +- **Where:** `src/filter_png.c:83-96` (`pt_explicit_length`) only looks at `media-col.media-size.y-dimension` and the top-level `media` keyword. +- **Trigger:** PAPPL accepts `media-col { media-size-name "om_12x50mm_12x50mm" }`. It validates the name against `media-supported` (`printer-ipp.c:2270-2294`), and `_papplMediaColImport` (`printer-support.c:551-559`) sets the 50 mm length from the name and prefers the name over x/y dimensions. +- **Consequence:** `pt_explicit_length` returns false, so `pt_set_page_length` overwrites the 50 mm with the image length (25.4 mm for 180 rows). This breaks "explicit geometry honored" for any IPP client that sends a size name rather than dimensions. CUPS sends dimensions, which is why your CUPS fixture passed. [verified by source path; not exercised by any test] +- **Same gap in the raster patch:** `patches/pappl-1.4.11-raster.patch:94-95` computes `explicit_width`/`explicit_length` from top-level `media` or `media-size` x/y only. So a contradictory size given by name is not rejected. This is less harmful because the raster page's own geometry still prints, but it misses the "contradictory explicit geometry fails visibly" requirement for this form. +- **Fix:** in both places, also treat `media-col.media-size-name` as explicit when `pwgMediaForPWG(name)` resolves with `length > 0`. Add one PNG case to `run_image`: `media-col` with only the size name should give 354 rows. + +**2. Low: idle status polling can mark the printer offline while a job is printing.** +- **Where:** `src/driver.c:588-606`. +- **Trigger:** PAPPL decides whether to run the status callback while holding its lock, then releases the lock and calls it (`printer-ipp.c:1783-1789`, `printer-accessors.c:528-532`). If a job starts in that gap, `papplPrinterOpenDevice` returns NULL. The new code then sets OFFLINE, sets media to unconfirmed, and logs "present but cannot be opened or claimed". The old code cleared OFFLINE in this case. +- **Consequence:** clients can briefly see a false `offline` reason and a misleading error log while a healthy job prints. The next idle poll clears it, and no output is affected. +- **Fix:** in the NULL-open branch, return without publishing when `papplPrinterGetState(printer) == IPP_PSTATE_PROCESSING`. Keep OFFLINE for the genuinely unclaimable case. + +**3. Low: some docs are stale and contradict the recorded evidence.** +- These still say integrated or recovery acceptance is pending: + - ADR-0008, last paragraph. + - ADR-0007: "Physical recovery remains part of the PT-2730 hardware acceptance gate". + - `tests/FINISHINGS.md`, last paragraph. + - `tests/CUT-PROBE.md`, intro and gate-result paragraph. + - `validation.md`, "Hardware gate": "remain pending". +- `validation.md` reports "39 of 42 overall" in one place and 41/42 elsewhere. +- `docs/adr/README.md` still lists the old titles for 0004 and 0008. +- Evidence is cited as `vault:/tmp/...` and local `/tmp/...`, which will not survive a reboot. +- **Fix:** update these before sync/archive, and move retained evidence somewhere durable or describe it as ephemeral. + +## Assumptions and gaps (need one check each) + +- **[assumption] Multi-copy PDF through CUPS may be rejected.** Without a `document-format`, PAPPL advertises `copies-supported 1-999` (`printer-ipp.c:77-84`). If the generated CUPS queue passes copies=N to the backend for a PDF it converts to PWG raster, the patched validation rejects copies≠1. CUPS's default error policy may then stop the queue. `check-cups.py` only sends PDFs with one copy. **Check:** `lp -n 2` on the two-label PDF through the driverless queue. If it fails, decide the behavior explicitly; quietly printing once would be worse. +- **[assumption] Custom lengths through CUPS may be rejected for 6/9/12/24 mm.** CUPS normally takes a single printer-wide minimum margin for custom sizes. Your published minimums are 53/57 centimm. For 12 mm that gives a left offset of 3 px, below `g.left_px` = 4, so `r_startpage` would reject it. This would be truthful but surprising. **Check:** `lp -o media=Custom.12x40mm label.png`. +- **[verified] JPEG geometry is not refreshed first.** PAPPL's JPEG filter builds print options before `r_startjob` refreshes the tape. After an offline start or an unpolled cassette change, a JPEG fails visibly with a width-mismatch error and sends no output. This behavior is not new, and the spec scenario only promises it for PNG. No action is needed beyond being aware of it. + +## Your two diagnostic nuances + +- **`job-impressions-completed=1` on the aborted incomplete job:** this is not a contract defect. PAPPL counts the page as soon as it starts, before any rows are decoded or any callback runs (`job-process.c:645`). It records input pages, not proven physical output. The authoritative signals are correct: job aborted, a format-error reason, no FF/EJECT, and the latch reporting `other` with the recovery log. No change is needed; at most, note this meaning in the README. +- **10 s client timeout on the streamed raster job:** this was a test-client setting, not a product defect. PAPPL replies to a streamed raster Print-Job only after end-job, so the reply time equals the physical print time. CUPS's backend waited through your multi-label PDF runs. Generic clients with short timeouts could report a failure for a job that actually completed. That is how PAPPL streaming works and predates this change, and nothing here replays the job. + +## Areas reviewed with no material issue + +- **Deferred FF / final EJECT:** a label's print command waits until the next label passes validation or end-job succeeds. A failed next-label validation leaves the run uncertain rather than finalizing it. +- **Precut and continuation:** ESC @ is sent only for a known closed run, and precut happens only on the first label of each batch. +- **Cleanup:** the run is marked uncertain before any setup is sent; cleanup is idempotent; a rejected or canceled follower that sent nothing leaves the previous run intact. +- **Finishings resolution:** enum handling covers 60+63, 63+60, none alongside other values, and 11/60 as equivalents; it is consistent with the patch's `_papplTrimSelection`. +- **Dependency patches:** the CUPS patch is correct; errors are cleared when a stream opens (`raster-stream.c:450`), so a failure does not carry over to the next job on the same thread. PAPPL regenerates its driver attributes only in `SetDriverData`, so the bridge's replacements survive `SetReadyMedia`. The version-pin guards fail closed. + +## Avoidable complexity + +- **[design preference] Duplicate lifecycle in the probe:** the cut probe's `batch` sequence (`tests/cut_probe.c` `run_batch`) is a second implementation of the lifecycle the driver now runs and that passed physical testing. After archive, consider trimming it to the historical diagnostic sequences, so the probe and driver cannot drift apart. +- **Redundant CI step:** the first workflow step (`target: build`) repeats the CTest run that the runtime image build already does through the same stage. It is harmless but redundant. +- **Accepted trade-off, not a defect:** rebuilding CUPS and pinning its version is the largest maintenance cost. Every Debian CUPS security release will turn CI red until the patch is refreshed, which blocks unrelated publishes in the meantime. You approved this. + +Nothing else I found adds substantial unnecessary machinery for your stated goals. diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/cups-review-fixes.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/cups-review-fixes.txt new file mode 100644 index 0000000..dd2fec4 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/cups-review-fixes.txt @@ -0,0 +1,1186 @@ +-- The C compiler identification is GNU 12.2.0 +-- Detecting C compiler ABI info +-- Detecting C compiler ABI info - done +-- Check for working C compiler: /usr/bin/cc - skipped +-- Detecting C compile features +-- Detecting C compile features - done +-- Found Python3: /usr/bin/python3 (found version "3.11.2") found components: Interpreter +-- Found PkgConfig: /usr/bin/pkg-config (found version "1.8.1") +-- Checking for module 'pappl' +-- Found pappl, version 1.4.11 +-- Checking for module 'libusb-1.0' +-- Found libusb-1.0, version 1.0.26 +-- Checking for module 'libpng' +-- Found libpng, version 1.6.39 +-- Configuring done +-- Generating done +-- Build files have been written to: /tmp/review-build +[ 27%] Building C object CMakeFiles/ptcore.dir/src/raster.c.o +[ 27%] Building C object CMakeFiles/ptcore.dir/src/protocol.c.o +[ 27%] Building C object CMakeFiles/ptcore.dir/src/tables.c.o +[ 36%] Building C object CMakeFiles/ptcore.dir/src/status.c.o +[ 45%] Building C object CMakeFiles/ptcore.dir/src/cutter.c.o +[ 54%] Linking C static library libptcore.a +[ 54%] Built target ptcore +[ 72%] Building C object CMakeFiles/ptouch-app.dir/src/main.c.o +[ 72%] Building C object CMakeFiles/ptouch-app.dir/src/device_usb.c.o +[ 81%] Building C object CMakeFiles/ptouch-app.dir/src/driver.c.o +[ 90%] Building C object CMakeFiles/ptouch-app.dir/src/filter_png.c.o +[100%] Linking C executable ptouch-app +[100%] Built target ptouch-app +"/tmp/cups-queue-epm9urlw/request.test": + Get-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + requested-attributes (keyword) = all + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 10218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + charset-configured (charset) = utf-8 + charset-supported (1setOf charset) = us-ascii,utf-8 + client-info-supported (1setOf keyword) = client-name,client-patches,client-string-version,client-version + compression-supported (1setOf keyword) = deflate,gzip,none + copies-default (integer) = 1 + device-uuid (uri) = urn:uuid:62ed3f5d-64a1-3080-57a1-bfcbe8c2bd2f + document-format-default (mimeMediaType) = application/octet-stream + generated-natural-language-supported (naturalLanguage) = en + ipp-versions-supported (1setOf keyword) = 1.1,2.0 + job-hold-until-default (keyword) = no-hold + job-hold-until-supported (1setOf keyword) = day-time,evening,indefinite,night,no-hold,second-shift,third-shift,weekend + job-hold-until-time-supported (boolean) = true + job-ids-supported (boolean) = true + job-k-octets-supported (rangeOfInteger) = 0-101803280 + job-priority-default (integer) = 50 + job-priority-supported (integer) = 1 + job-sheets-default (nameWithoutLanguage) = none + job-sheets-supported (nameWithoutLanguage) = none + multiple-document-handling-supported (1setOf keyword) = separate-documents-uncollated-copies,separate-documents-collated-copies + multiple-document-jobs-supported (boolean) = false + multiple-operation-time-out (integer) = 60 + multiple-operation-time-out-action (keyword) = abort-job + natural-language-configured (naturalLanguage) = en + notify-events-default (keyword) = job-completed + notify-events-supported (1setOf keyword) = document-completed,document-config-changed,document-created,document-fetchable,document-state-changed,document-stopped,job-completed,job-config-changed,job-created,job-fetchable,job-progress,job-state-changed,job-stopped,printer-config-changed,printer-finishings-changed,printer-media-changed,printer-queue-order-changed,printer-restarted,printer-shutdown,printer-state-changed,printer-stopped,resource-canceled,resource-config-changed,resource-created,resource-installed,resource-changed,printer-created,printer-deleted,system-config-changed,system-state-changed,system-stopped + notify-lease-duration-default (integer) = 3600 + notify-lease-duration-supported (rangeOfInteger) = 0-86400 + notify-max-events-supported (integer) = 100 + notify-pull-method-supported (keyword) = ippget + operations-supported (1setOf enum) = Print-Job,Validate-Job,Create-Job,Send-Document,Cancel-Job,Get-Job-Attributes,Get-Jobs,Get-Printer-Attributes,Pause-Printer,Resume-Printer,Set-Printer-Attributes,Get-Printer-Supported-Values,Create-Printer-Subscriptions,Create-Job-Subscriptions,Get-Subscription-Attributes,Get-Subscriptions,Renew-Subscription,Cancel-Subscription,Get-Notifications,Enable-Printer,Disable-Printer,Pause-Printer-After-Current-Job,Cancel-Current-Job,Cancel-Jobs,Cancel-My-Jobs,Close-Job,Identify-Printer,Hold-Job,Release-Job,Hold-New-Jobs,Release-Held-New-Jobs + orientation-requested-supported (1setOf enum) = portrait,landscape,reverse-landscape,reverse-portrait,none + pdl-override-supported (keyword) = attempted + print-content-optimize-supported (1setOf keyword) = auto,graphic,photo,text-and-graphic,text + print-processing-attributes-supported (1setOf keyword) = print-color-mode,printer-resolution + print-quality-supported (1setOf enum) = draft,normal,high + print-scaling-supported (1setOf keyword) = auto,auto-fit,fill,fit,none + printer-get-attributes-supported (keyword) = document-format + printer-info (textWithoutLanguage) = ptouch + printer-name (nameWithoutLanguage) = ptouch + printer-uuid (uri) = urn:uuid:62ed3f5d-64a1-3080-57a1-bfcbe8c2bd2f + requesting-user-uri-supported (boolean) = true + smi55357-device-uri (uri) = socket://127.0.0.1:54813 + smi55357-driver (keyword) = brother_ptouch + uri-security-supported (1setOf keyword) = tls,none + which-jobs-supported (1setOf keyword) = completed,not-completed,all + printer-id (integer) = 1 + color-supported (boolean) = false + document-format-preferred (mimeMediaType) = image/urf + document-format-supported (1setOf mimeMediaType) = application/octet-stream,image/pwg-raster,image/urf,image/png,image/jpeg + ipp-features-supported (keyword) = ipp-everywhere + job-creation-attributes-supported (1setOf keyword) = copies,document-format,document-name,ipp-attribute-fidelity,job-hold-until,job-hold-until-time,job-name,job-priority,job-retain-until,job-retain-until-interval,job-retain-until-time,media,media-col,multiple-document-handling,orientation-requested,print-color-mode,print-content-optimize,print-quality,printer-resolution,finishings,finishings-col + jpeg-features-supported (1setOf keyword) = arithmetic,cmyk,progressive + jpeg-k-octets-supported (rangeOfInteger) = 0-101803280 + jpeg-x-dimension-supported (rangeOfInteger) = 0-16384 + jpeg-y-dimension-supported (rangeOfInteger) = 1-16384 + landscape-orientation-requested-preferred (enum) = 4 + media-bottom-margin-supported (integer) = 0 + media-col-supported (1setOf keyword) = media-bottom-margin,media-left-margin,media-right-margin,media-size,media-size-name,media-top-margin,media-source,media-type + media-source-supported (1setOf keyword) = main-roll,auto + media-supported (1setOf keyword) = om_6x25mm_6x25mm,om_6x50mm_6x50mm,om_6x100mm_6x100mm,om_9x25mm_9x25mm,om_9x50mm_9x50mm,om_9x100mm_9x100mm,om_12x25mm_12x25mm,om_12x50mm_12x50mm,om_12x100mm_12x100mm,om_18x25mm_18x25mm,om_18x50mm_18x50mm,om_18x100mm_18x100mm,om_24x25mm_24x25mm,om_24x50mm_24x50mm,om_24x100mm_24x100mm,roll_min_6x5mm,roll_max_24x300mm + media-top-margin-supported (integer) = 0 + media-type-supported (keyword) = labels + mopria-certified (textWithoutLanguage) = 1.3 + output-bin-supported (keyword) = face-down + pages-per-minute (integer) = 20 + print-color-mode-supported (1setOf keyword) = bi-level,monochrome + printer-device-id (textWithoutLanguage) = MFG:Brother;MDL:PT-2730;CMD:PT-CBP; + printer-kind (keyword) = label + printer-make-and-model (textWithoutLanguage) = Brother PT-2730 + printer-output-tray (octetString) = type=unRemovableBin;maxcapacity=-2;remaining=-2;status=0;name=face-down;stackingorder=firstToLast;pagedelivery=faceDown; + printer-resolution-supported (resolution) = 180dpi + printer-settable-attributes (1setOf keyword) = copies-default,media-col-default,media-col-ready,media-default,media-ready,multiple-document-handling-default,orientation-requested-default,print-color-mode-default,print-content-optimize-default,print-quality-default,printer-contact-col,printer-geo-location,printer-location,printer-organization,printer-organizational-unit,printer-resolution-default,finishings-col-default,finishings-default + pwg-raster-document-resolution-supported (resolution) = 180dpi + pwg-raster-document-type-supported (1setOf keyword) = black_1,black_8 + sides-supported (keyword) = one-sided + urf-supported (1setOf keyword) = V1.5,W8,DEVW8,PQ3-4-5,FN3-11,IS0-9,MT5,OB10,RS180 + finishings-supported (1setOf enum) = none,trim,trim-after-pages,trim-after-job + finishings-ready (1setOf enum) = none,trim,trim-after-pages,trim-after-job + finishings-col-supported (keyword) = finishing-template + finishing-template-supported (1setOf keyword) = trim,trim-after-pages,trim-after-job,ptouch-chain + finishings-col-database (1setOf collection) = {finishing-template=trim},{finishing-template=trim-after-pages},{finishing-template=trim-after-job},{finishing-template=ptouch-chain} + finishings-col-ready (1setOf collection) = {finishing-template=trim},{finishing-template=trim-after-pages},{finishing-template=trim-after-job},{finishing-template=ptouch-chain} + media-size-supported (1setOf collection) = {x-dimension=599-601 y-dimension=500-30000},{x-dimension=899-901 y-dimension=500-30000},{x-dimension=1199-1201 y-dimension=500-30000},{x-dimension=1799-1801 y-dimension=500-30000},{x-dimension=2399-2401 y-dimension=500-30000} + media-left-margin-supported (1setOf integer) = 53,63,74,83,297 + media-right-margin-supported (1setOf integer) = 57,71,74,85,297 + finishings-default (enum) = trim + finishings-col-default (collection) = {finishing-template=trim} + printer-is-accepting-jobs (boolean) = true + printer-state (enum) = idle + printer-state-message (textWithoutLanguage) = Idle. + printer-state-reasons (keyword) = none + copies-supported (rangeOfInteger) = 1-999 + identify-actions-default (keyword) = none + job-spooling-supported (keyword) = spool + media-col-default (collection) = {media-bottom-margin=0 media-left-margin=63 media-right-margin=71 media-size={x-dimension=1200 y-dimension=10000} media-size-name=om_12x100mm_12x100mm media-source=main-roll media-top-margin=0 media-type=labels} + media-col-ready (collection) = {media-bottom-margin=0 media-left-margin=63 media-right-margin=71 media-size={x-dimension=1200 y-dimension=10000} media-size-name=om_12x100mm_12x100mm media-source=main-roll media-top-margin=0 media-type=labels} + media-default (keyword) = om_12x100mm_12x100mm + media-ready (keyword) = om_12x100mm_12x100mm + multiple-document-handling-default (keyword) = separate-documents-collated-copies + orientation-requested-default (enum) = none + output-bin-default (keyword) = face-down + print-color-mode-default (keyword) = bi-level + print-content-optimize-default (keyword) = auto + print-quality-default (enum) = normal + print-scaling-default (keyword) = none + printer-config-change-date-time (dateTime) = 2026-10-13T05:03:12Z + printer-config-change-time (integer) = 1 + printer-contact-col (collection) = {contact-name= contact-uri=data:, contact-vcard=BEGIN:VCARD +VERSION:4.0 +FN: +TEL;VALUE=URI;TYPE=work: +EMAIL;TYPE=work: +END:VCARD +} + printer-current-time (dateTime) = 2026-10-13T05:03:13Z + printer-dns-sd-name (nameWithoutLanguage) = ptouch + printer-firmware-name (1setOf nameWithoutLanguage) = ptouch-app,PAPPL,libcups,libjpeg,libpng + printer-firmware-patches (1setOf textWithoutLanguage) = ,,,, + printer-firmware-string-version (1setOf textWithoutLanguage) = 1.0,1.4.11,2.4.2,8.0,1.6.39 + printer-firmware-version (1setOf octetString) = \001\000\000\000\000\000\000\000,\001\000\004\000\ \000\000\000,\002\000\004\000\002\000\000\000,\010\000\000\000\000\000\000\000,\001\000\006\000'\000\000\000 + printer-geo-location (unknown) = unknown + printer-icons (1setOf uri) = http://localhost:8000/ptouch/icon-sm.png,http://localhost:8000/ptouch/icon-md.png,http://localhost:8000/ptouch/icon-lg.png + printer-impressions-completed (integer) = 0 + printer-input-tray (1setOf octetString) = type=sheetFeedAutoRemovableTray;mediafeed=10000;mediaxfeed=1200;maxcapacity=-2;level=-2;status=0;name=main-roll;,type=other;mediafeed=0;mediaxfeed=0;maxcapacity=-2;level=-2;status=0;name=auto; + printer-location (textWithoutLanguage) = + printer-more-info (uri) = http://localhost:8000/ptouch/ + printer-organization (textWithoutLanguage) = + printer-organizational-unit (textWithoutLanguage) = + printer-resolution-default (resolution) = 180dpi + printer-speed-default (integer) = 0 + printer-state-change-date-time (dateTime) = 2026-10-13T05:03:12Z + printer-state-change-time (integer) = 0 + printer-supply-info-uri (uri) = http://localhost:8000/ptouch/supplies + printer-up-time (integer) = 1 + printer-uri-supported (uri) = ipp://localhost:8000/ipp/print/ptouch + printer-xri-supported (collection) = {xri-authentication=none xri-security=none xri-uri=ipp://localhost:8000/ipp/print/ptouch} + queued-job-count (integer) = 0 + sides-default (keyword) = one-sided + uri-authentication-supported (keyword) = none + +"/tmp/cups-queue-epm9urlw/request.test": + Set-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + finishings-default (enum) = trim + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 114 bytes in response + status-code = successful-ok (Printer attributes set.) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + status-message (textWithoutLanguage) = Printer attributes set. + +"/tmp/cups-queue-epm9urlw/request.test": + Get-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + requested-attributes (keyword) = finishings-default + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 100 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + finishings-default (enum) = trim + +"/tmp/cups-queue-epm9urlw/request.test": + Set-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + finishings-default (enum) = trim-after-job + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 114 bytes in response + status-code = successful-ok (Printer attributes set.) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + status-message (textWithoutLanguage) = Printer attributes set. + +"/tmp/cups-queue-epm9urlw/request.test": + Get-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + requested-attributes (keyword) = finishings-default + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 100 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + finishings-default (enum) = trim-after-job + +"/tmp/cups-queue-epm9urlw/request.test": + Set-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + finishings-default (enum) = trim + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 114 bytes in response + status-code = successful-ok (Printer attributes set.) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + status-message (textWithoutLanguage) = Printer attributes set. + + + +PageSize/Media Size: *12x100mm 12x25mm 12x50mm 18x100mm 18x25mm 18x50mm 24x100mm 24x25mm 24x50mm 6x100mm 6x25mm 6x50mm 9x100mm 9x25mm 9x50mm Custom.WIDTHxHEIGHT +InputSlot/Media Source: *MainRoll Auto +cupsPrintQuality/cupsPrintQuality: Draft *Normal High +ColorModel/Output Mode: *Gray DeviceGray +OutputBin/OutputBin: *FaceDown +CutMedia/CutMedia: None *Auto EndOfPage EndOfJob +cupsFinishingTemplate/cupsFinishingTemplate: *none Trim TrimAfterPages TrimAfterJob PtouchChain + +request id is ptouch-cups-1 (1 file(s)) + +request id is ptouch-cups-2 (1 file(s)) + +Offline jobs: ptouch-cups-1 root 1024 Tue Oct 13 05:03:13 2026 +ptouch-cups-2 root 1024 Tue Oct 13 05:03:13 2026 + +Resumed offline job 1 {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +Resumed offline job 2 {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +request id is ptouch-cups-3 (1 file(s)) + +CUPS label.png None {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +request id is ptouch-cups-4 (1 file(s)) + +CUPS label.png 60 {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +request id is ptouch-cups-5 (1 file(s)) + +CUPS label.jpg 63 {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +request id is ptouch-cups-6 (1 file(s)) + +CUPS labels.pdf 60 {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'ff', 'autocut': True, 'chain_off_at_print': False}, {'rows': 354, 'length_mm': 49.95, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +request id is ptouch-cups-7 (1 file(s)) + +CUPS labels.pdf 63 {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'ff', 'autocut': True, 'chain_off_at_print': False}, {'rows': 354, 'length_mm': 49.95, 'finish': 'eject', 'autocut': False, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +request id is ptouch-cups-8 (1 file(s)) + +CUPS labels.pdf 60 {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'ff', 'autocut': True, 'chain_off_at_print': False}, {'rows': 354, 'length_mm': 49.95, 'finish': 'ff', 'autocut': True, 'chain_off_at_print': False}, {'rows': 354, 'length_mm': 49.95, 'finish': 'ff', 'autocut': True, 'chain_off_at_print': False}, {'rows': 354, 'length_mm': 49.95, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +request id is ptouch-cups-9 (1 file(s)) + +CUPS rounded custom size with explicit margins: {'pages': [{'rows': 282, 'length_mm': 39.79, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +request id is ptouch-cups-10 (1 file(s)) + +CUPS oversized custom PNG rejected without output. +"/tmp/cups-queue-epm9urlw/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + print-scaling (keyword) = none + media-col (collection) = {media-size={x-dimension=1200 y-dimension=4000} media-left-margin=63 media-right-margin=71} + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 11 + job-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch/11 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 11 + /tmp/cups-queue-epm9urlw/request [0001] + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 908 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + print-scaling (keyword) = none + media-col (collection) = {media-size={x-dimension=1200 y-dimension=4000} media-left-margin=63 media-right-margin=71} + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 11 + job-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch/11 + job-uuid (uri) = urn:uuid:1a25b94b-98eb-328b-76ab-a582928656e5 + job-printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:03:26Z + date-time-at-completed (dateTime) = 2026-10-13T05:03:26Z + date-time-at-processing (dateTime) = 2026-10-13T05:03:26Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 1 + job-printer-up-time (integer) = 19 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 14 + time-at-completed (integer) = 14 + time-at-processing (integer) = 14 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +Exact standard custom media-col: {'pages': [{'rows': 283, 'length_mm': 39.93, 'finish': 'eject', 'autocut': True, 'chain_off_at_print': True}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 1} +"/tmp/cups-queue-epm9urlw/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 1 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings-col (collection) = {finishing-template=ptouch-chain} + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 12 + job-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch/12 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 12 + /tmp/cups-queue-epm9urlw/request [0001] + /tmp/cups-queue-epm9urlw/request [PASS] + RECEIVED: 858 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 1 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings-col (collection) = {finishing-template=ptouch-chain} + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 12 + job-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch/12 + job-uuid (uri) = urn:uuid:6ee7d6e8-d869-313a-770b-16b9873af066 + job-printer-uri (uri) = ipp://127.0.0.1:8000/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:03:31Z + date-time-at-completed (dateTime) = 2026-10-13T05:03:31Z + date-time-at-processing (dateTime) = 2026-10-13T05:03:31Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 1 + job-printer-up-time (integer) = 24 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 19 + time-at-completed (integer) = 19 + time-at-processing (integer) = 19 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +Standard ptouch-chain collection: {'pages': [{'rows': 354, 'length_mm': 49.95, 'finish': 'ff', 'autocut': True, 'chain_off_at_print': False}], 'terminal_commands_after_pages': [], 'init': 1, 'eject': 0} +Conventional CUPS integration passed. +I [2026-10-13T05:03:12.641Z] Starting log, system up 0 second(s), 0 printer(s), listening for connections on 'd7ee03dcb216.local:8000' from up to 32768 clients. +I [2026-10-13T05:03:12.644Z] Listening for connections on '*:8000'. +I [2026-10-13T05:03:12.644Z] Listening for connections on '*:8000'. +I [2026-10-13T05:03:12.644Z] Printer 'ptouch' at resource path '/ipp/print/ptouch'. +W [2026-10-13T05:03:12.644Z] [Printer ptouch] Driver does not support identification. +W [2026-10-13T05:03:12.644Z] [Printer ptouch] Driver does not support a self-test page. +W [2026-10-13T05:03:12.644Z] [Printer ptouch] Driver does not provide printer icons, using defaults. +I [2026-10-13T05:03:12.644Z] Created printer for socket://127.0.0.1:54813 (PTOUCH_DEVICE_URI override) +I [2026-10-13T05:03:12.644Z] Listening for connections on '/run/ptouch-app.sock'. +I [2026-10-13T05:03:12.644Z] Starting system. +E [2026-10-13T05:03:12.644Z] Unable to initialize DNS-SD: Daemon not running +E [2026-10-13T05:03:12.644Z] Unable to initialize DNS-SD: Daemon not running +I [2026-10-13T05:03:13.645Z] [Client 1] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.646Z] [Client 1] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.646Z] [Client 1] Continue +I [2026-10-13T05:03:13.646Z] [Client 1] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:13.646Z] [Client 1] OK application/ipp 10218 +I [2026-10-13T05:03:13.654Z] [Client 1] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.660Z] [Client 2] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.660Z] [Client 2] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.660Z] [Client 2] Continue +I [2026-10-13T05:03:13.660Z] [Client 2] Set-Printer-Attributes successful-ok (Printer attributes set.) +I [2026-10-13T05:03:13.660Z] [Client 2] OK application/ipp 114 +I [2026-10-13T05:03:13.661Z] [Client 2] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.669Z] [Client 3] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.669Z] [Client 3] HTTP_STATE_GET http://127.0.0.1:8000/ptouch/printing HTTP/1.1 () +I [2026-10-13T05:03:13.669Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:13.669Z] [Client 3] OK text/html 0 +I [2026-10-13T05:03:13.670Z] [Client 3] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.670Z] [Client 4] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.670Z] [Client 4] HTTP_STATE_POST http://127.0.0.1:8000/ptouch/printing HTTP/1.1 () +I [2026-10-13T05:03:13.670Z] [Client 4] OK text/html 0 +I [2026-10-13T05:03:13.670Z] [Client 4] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.675Z] [Client 5] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.675Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:13.675Z] [Client 5] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.675Z] [Client 5] Continue +I [2026-10-13T05:03:13.676Z] [Client 5] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:13.676Z] [Client 5] OK application/ipp 100 +I [2026-10-13T05:03:13.676Z] [Client 5] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.681Z] [Client 6] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.682Z] [Client 6] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.682Z] [Client 6] Continue +I [2026-10-13T05:03:13.682Z] [Client 6] Set-Printer-Attributes successful-ok (Printer attributes set.) +I [2026-10-13T05:03:13.682Z] [Client 6] OK application/ipp 114 +I [2026-10-13T05:03:13.682Z] [Client 6] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.683Z] [Client 7] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.683Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:13.683Z] [Client 7] HTTP_STATE_GET http://127.0.0.1:8000/ptouch/printing HTTP/1.1 () +I [2026-10-13T05:03:13.683Z] [Client 7] OK text/html 0 +I [2026-10-13T05:03:13.684Z] [Client 7] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.684Z] [Client 8] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.684Z] [Client 8] HTTP_STATE_POST http://127.0.0.1:8000/ptouch/printing HTTP/1.1 () +I [2026-10-13T05:03:13.684Z] [Client 8] OK text/html 0 +I [2026-10-13T05:03:13.684Z] [Client 8] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.689Z] [Client 9] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.689Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:13.689Z] [Client 9] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.689Z] [Client 9] Continue +I [2026-10-13T05:03:13.690Z] [Client 9] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:13.690Z] [Client 9] OK application/ipp 100 +I [2026-10-13T05:03:13.690Z] [Client 9] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.695Z] [Client 10] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.695Z] [Client 10] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.695Z] [Client 10] Continue +I [2026-10-13T05:03:13.695Z] [Client 10] Set-Printer-Attributes successful-ok (Printer attributes set.) +I [2026-10-13T05:03:13.695Z] [Client 10] OK application/ipp 114 +I [2026-10-13T05:03:13.695Z] [Client 10] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.701Z] [Client 11] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.701Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:13.701Z] [Client 11] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.701Z] [Client 11] Continue +I [2026-10-13T05:03:13.701Z] [Client 11] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:13.701Z] [Client 11] OK application/ipp 14378 +I [2026-10-13T05:03:13.707Z] [Client 11] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.738Z] [Client 12] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.739Z] [Client 12] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.739Z] [Client 12] Continue +I [2026-10-13T05:03:13.739Z] [Client 12] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:13.739Z] [Client 12] OK application/ipp 1033 +I [2026-10-13T05:03:13.740Z] [Client 12] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.740Z] [Client 12] Continue +I [2026-10-13T05:03:13.740Z] [Client 12] Validate-Job successful-ok +I [2026-10-13T05:03:13.740Z] [Client 12] OK application/ipp 75 +I [2026-10-13T05:03:13.740Z] [Client 13] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.740Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.740Z] [Client 13] Continue +I [2026-10-13T05:03:13.740Z] [Client 12] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.740Z] [Client 12] Continue +I [2026-10-13T05:03:13.740Z] [Client 13] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:13.740Z] [Client 13] OK application/ipp 1033 +I [2026-10-13T05:03:13.740Z] [Client 12] Create-Job successful-ok +I [2026-10-13T05:03:13.740Z] [Client 12] OK application/ipp 237 +I [2026-10-13T05:03:13.740Z] [Client 12] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.740Z] [Client 12] Continue +I [2026-10-13T05:03:13.740Z] [Job 1] Received 153 bytes of document data. +I [2026-10-13T05:03:13.741Z] [Client 12] Send-Document successful-ok +I [2026-10-13T05:03:13.741Z] [Client 12] OK application/ipp 220 +I [2026-10-13T05:03:13.741Z] [Job 1] Starting print job. +E [2026-10-13T05:03:13.741Z] [Device] Unable to connect to \'127.0.0.1:54813\': Host is down +E [2026-10-13T05:03:13.741Z] [Printer ptouch] Unable to open device 'socket://127.0.0.1:54813', pausing queue until printer becomes available. +I [2026-10-13T05:03:13.741Z] [Client 12] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.741Z] [Client 12] Continue +I [2026-10-13T05:03:13.741Z] [Client 12] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:13.741Z] [Client 12] OK application/ipp 1038 +I [2026-10-13T05:03:13.741Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.741Z] [Client 13] Continue +I [2026-10-13T05:03:13.741Z] [Client 13] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:13.741Z] [Client 13] OK application/ipp 233 +I [2026-10-13T05:03:13.741Z] [Client 12] Closing connection from 'localhost'. +I [2026-10-13T05:03:13.741Z] [Client 14] Accepted connection from 'localhost'. +I [2026-10-13T05:03:13.741Z] [Client 14] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:13.741Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:13.741Z] [Client 14] Continue +I [2026-10-13T05:03:13.741Z] [Client 14] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:13.741Z] [Client 14] OK application/ipp 233 +I [2026-10-13T05:03:14.741Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:14.742Z] [Client 13] Continue +I [2026-10-13T05:03:14.742Z] [Client 13] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:14.742Z] [Client 13] OK application/ipp 1038 +I [2026-10-13T05:03:14.742Z] [Client 14] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:14.742Z] [Client 14] Continue +I [2026-10-13T05:03:14.742Z] [Client 14] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:14.742Z] [Client 14] OK application/ipp 1038 +I [2026-10-13T05:03:14.742Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:14.742Z] [Client 13] Continue +I [2026-10-13T05:03:14.742Z] [Client 13] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:14.742Z] [Client 13] OK application/ipp 233 +I [2026-10-13T05:03:14.742Z] [Client 14] Closing connection from 'localhost'. +I [2026-10-13T05:03:14.742Z] [Client 15] Accepted connection from 'localhost'. +I [2026-10-13T05:03:14.742Z] [Client 15] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:14.742Z] [Client 15] Continue +I [2026-10-13T05:03:14.742Z] [Client 15] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:14.742Z] [Client 15] OK application/ipp 233 +I [2026-10-13T05:03:15.742Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:15.742Z] [Client 13] Continue +I [2026-10-13T05:03:15.742Z] [Client 13] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:15.742Z] [Client 13] OK application/ipp 1038 +I [2026-10-13T05:03:15.743Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:15.743Z] [Client 13] Continue +I [2026-10-13T05:03:15.743Z] [Client 13] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:15.743Z] [Client 13] OK application/ipp 233 +I [2026-10-13T05:03:15.743Z] [Client 15] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:15.743Z] [Client 15] Continue +I [2026-10-13T05:03:15.743Z] [Client 15] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:15.743Z] [Client 15] OK application/ipp 1038 +I [2026-10-13T05:03:15.743Z] [Client 15] Closing connection from 'localhost'. +I [2026-10-13T05:03:15.743Z] [Client 16] Accepted connection from 'localhost'. +I [2026-10-13T05:03:15.743Z] [Client 16] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:15.743Z] [Client 16] Continue +I [2026-10-13T05:03:15.744Z] [Client 16] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:15.744Z] [Client 16] OK application/ipp 233 +I [2026-10-13T05:03:17.743Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:17.743Z] [Client 13] Continue +I [2026-10-13T05:03:17.743Z] [Client 13] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:17.743Z] [Client 13] OK application/ipp 1038 +I [2026-10-13T05:03:17.743Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:17.743Z] [Client 13] Continue +I [2026-10-13T05:03:17.743Z] [Client 13] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:17.743Z] [Client 13] OK application/ipp 233 +I [2026-10-13T05:03:17.744Z] [Client 16] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:17.744Z] [Client 16] Continue +I [2026-10-13T05:03:17.744Z] [Client 16] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:17.744Z] [Client 16] OK application/ipp 1038 +I [2026-10-13T05:03:17.744Z] [Client 16] Closing connection from 'localhost'. +I [2026-10-13T05:03:17.744Z] [Client 17] Accepted connection from 'localhost'. +I [2026-10-13T05:03:17.744Z] [Client 17] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:17.744Z] [Client 17] Continue +I [2026-10-13T05:03:17.745Z] [Client 17] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:17.745Z] [Client 17] OK application/ipp 233 +I [2026-10-13T05:03:18.741Z] [Job 1] PNG image dimensions are 76x180x1 +I [2026-10-13T05:03:18.741Z] [Job 1] PNG image resolution is 180x180dpi +I [2026-10-13T05:03:18.741Z] [Job 1] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:03:18.744Z] [Job 1] Completed, job-impressions-completed=1. +I [2026-10-13T05:03:20.744Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.744Z] [Client 13] Continue +I [2026-10-13T05:03:20.744Z] [Client 13] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:20.744Z] [Client 13] OK application/ipp 1033 +I [2026-10-13T05:03:20.744Z] [Client 13] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.744Z] [Client 13] Continue +I [2026-10-13T05:03:20.744Z] [Client 13] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:20.744Z] [Client 13] OK application/ipp 247 +I [2026-10-13T05:03:20.745Z] [Client 17] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.745Z] [Client 17] Continue +I [2026-10-13T05:03:20.745Z] [Client 17] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:20.745Z] [Client 17] OK application/ipp 1033 +I [2026-10-13T05:03:20.745Z] [Client 17] Closing connection from 'localhost'. +I [2026-10-13T05:03:20.745Z] [Client 18] Accepted connection from 'localhost'. +I [2026-10-13T05:03:20.745Z] [Client 18] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.745Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:20.745Z] [Client 18] Continue +I [2026-10-13T05:03:20.746Z] [Client 18] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:20.746Z] [Client 18] OK application/ipp 247 +I [2026-10-13T05:03:20.746Z] [Client 18] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.746Z] [Client 18] Continue +I [2026-10-13T05:03:20.746Z] [Client 18] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:20.746Z] [Client 18] OK application/ipp 1033 +I [2026-10-13T05:03:20.746Z] [Client 18] Closing connection from 'localhost'. +I [2026-10-13T05:03:20.747Z] [Client 13] Closing connection from 'localhost'. +I [2026-10-13T05:03:20.753Z] [Client 19] Accepted connection from 'localhost'. +I [2026-10-13T05:03:20.754Z] [Client 19] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.754Z] [Client 19] Continue +I [2026-10-13T05:03:20.754Z] [Client 19] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:20.754Z] [Client 19] OK application/ipp 1033 +I [2026-10-13T05:03:20.754Z] [Client 19] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.754Z] [Client 19] Continue +I [2026-10-13T05:03:20.755Z] [Client 20] Accepted connection from 'localhost'. +I [2026-10-13T05:03:20.755Z] [Client 19] Validate-Job successful-ok +I [2026-10-13T05:03:20.755Z] [Client 19] OK application/ipp 75 +I [2026-10-13T05:03:20.755Z] [Client 20] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.755Z] [Client 20] Continue +I [2026-10-13T05:03:20.755Z] [Client 19] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.755Z] [Client 19] Continue +I [2026-10-13T05:03:20.755Z] [Client 19] Create-Job successful-ok +I [2026-10-13T05:03:20.755Z] [Client 19] OK application/ipp 237 +I [2026-10-13T05:03:20.755Z] [Client 20] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:20.755Z] [Client 20] OK application/ipp 1033 +I [2026-10-13T05:03:20.755Z] [Client 19] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.755Z] [Client 19] Continue +I [2026-10-13T05:03:20.755Z] [Job 2] Received 153 bytes of document data. +I [2026-10-13T05:03:20.756Z] [Client 19] Send-Document successful-ok +I [2026-10-13T05:03:20.756Z] [Client 19] OK application/ipp 220 +I [2026-10-13T05:03:20.756Z] [Job 2] Starting print job. +I [2026-10-13T05:03:20.756Z] [Job 2] PNG image dimensions are 76x180x1 +I [2026-10-13T05:03:20.756Z] [Job 2] PNG image resolution is 180x180dpi +I [2026-10-13T05:03:20.756Z] [Client 19] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.756Z] [Client 19] Continue +I [2026-10-13T05:03:20.756Z] [Job 2] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:03:20.756Z] [Client 19] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:20.756Z] [Client 19] OK application/ipp 1037 +I [2026-10-13T05:03:20.756Z] [Client 19] Closing connection from 'localhost'. +I [2026-10-13T05:03:20.756Z] [Client 21] Accepted connection from 'localhost'. +I [2026-10-13T05:03:20.756Z] [Client 21] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:20.756Z] [Client 21] Continue +I [2026-10-13T05:03:20.756Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:20.756Z] [Client 21] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:20.756Z] [Client 21] OK application/ipp 233 +I [2026-10-13T05:03:20.759Z] [Job 2] Completed, job-impressions-completed=1. +I [2026-10-13T05:03:21.755Z] [Client 20] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.756Z] [Client 20] Continue +I [2026-10-13T05:03:21.756Z] [Client 20] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:21.756Z] [Client 20] OK application/ipp 1033 +I [2026-10-13T05:03:21.757Z] [Client 21] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.757Z] [Client 21] Continue +I [2026-10-13T05:03:21.757Z] [Client 20] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.757Z] [Client 20] Continue +I [2026-10-13T05:03:21.757Z] [Client 21] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:21.757Z] [Client 21] OK application/ipp 1033 +I [2026-10-13T05:03:21.757Z] [Client 20] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:21.757Z] [Client 20] OK application/ipp 247 +I [2026-10-13T05:03:21.758Z] [Client 21] Closing connection from 'localhost'. +I [2026-10-13T05:03:21.758Z] [Client 22] Accepted connection from 'localhost'. +I [2026-10-13T05:03:21.758Z] [Client 22] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.758Z] [Client 22] Continue +I [2026-10-13T05:03:21.758Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:21.758Z] [Client 22] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:21.758Z] [Client 22] OK application/ipp 247 +I [2026-10-13T05:03:21.759Z] [Client 22] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.759Z] [Client 22] Continue +I [2026-10-13T05:03:21.759Z] [Client 22] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:21.759Z] [Client 22] OK application/ipp 1033 +I [2026-10-13T05:03:21.759Z] [Client 22] Closing connection from 'localhost'. +I [2026-10-13T05:03:21.760Z] [Client 20] Closing connection from 'localhost'. +I [2026-10-13T05:03:21.850Z] [Client 23] Accepted connection from 'localhost'. +I [2026-10-13T05:03:21.850Z] [Client 23] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.850Z] [Client 23] Continue +I [2026-10-13T05:03:21.851Z] [Client 23] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:21.851Z] [Client 23] OK application/ipp 1033 +I [2026-10-13T05:03:21.852Z] [Client 23] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.852Z] [Client 23] Continue +I [2026-10-13T05:03:21.852Z] [Client 24] Accepted connection from 'localhost'. +I [2026-10-13T05:03:21.852Z] [Client 23] Validate-Job successful-ok +I [2026-10-13T05:03:21.852Z] [Client 23] OK application/ipp 75 +I [2026-10-13T05:03:21.852Z] [Client 24] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.852Z] [Client 24] Continue +I [2026-10-13T05:03:21.852Z] [Client 23] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.852Z] [Client 23] Continue +I [2026-10-13T05:03:21.852Z] [Client 23] Create-Job successful-ok +I [2026-10-13T05:03:21.852Z] [Client 23] OK application/ipp 237 +I [2026-10-13T05:03:21.852Z] [Client 24] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:21.852Z] [Client 24] OK application/ipp 1033 +I [2026-10-13T05:03:21.853Z] [Client 23] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.853Z] [Client 23] Continue +I [2026-10-13T05:03:21.853Z] [Job 3] Received 153 bytes of document data. +I [2026-10-13T05:03:21.853Z] [Client 23] Send-Document successful-ok +I [2026-10-13T05:03:21.853Z] [Client 23] OK application/ipp 220 +I [2026-10-13T05:03:21.853Z] [Job 3] Starting print job. +I [2026-10-13T05:03:21.853Z] [Job 3] PNG image dimensions are 76x180x1 +I [2026-10-13T05:03:21.853Z] [Job 3] PNG image resolution is 180x180dpi +I [2026-10-13T05:03:21.853Z] [Job 3] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:03:21.853Z] [Client 23] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.853Z] [Client 23] Continue +I [2026-10-13T05:03:21.853Z] [Client 23] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:21.853Z] [Client 23] OK application/ipp 1037 +I [2026-10-13T05:03:21.854Z] [Client 23] Closing connection from 'localhost'. +I [2026-10-13T05:03:21.854Z] [Client 25] Accepted connection from 'localhost'. +I [2026-10-13T05:03:21.854Z] [Client 25] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:21.854Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:21.854Z] [Client 25] Continue +I [2026-10-13T05:03:21.854Z] [Client 25] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:21.854Z] [Client 25] OK application/ipp 233 +I [2026-10-13T05:03:21.856Z] [Job 3] Completed, job-impressions-completed=1. +I [2026-10-13T05:03:22.853Z] [Client 24] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.853Z] [Client 24] Continue +I [2026-10-13T05:03:22.853Z] [Client 24] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:22.853Z] [Client 24] OK application/ipp 1033 +I [2026-10-13T05:03:22.853Z] [Client 24] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.854Z] [Client 24] Continue +I [2026-10-13T05:03:22.854Z] [Client 24] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:22.854Z] [Client 24] OK application/ipp 247 +I [2026-10-13T05:03:22.854Z] [Client 25] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.854Z] [Client 25] Continue +I [2026-10-13T05:03:22.854Z] [Client 25] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:22.855Z] [Client 25] OK application/ipp 1033 +I [2026-10-13T05:03:22.855Z] [Client 25] Closing connection from 'localhost'. +I [2026-10-13T05:03:22.855Z] [Client 26] Accepted connection from 'localhost'. +I [2026-10-13T05:03:22.855Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:22.855Z] [Client 26] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.855Z] [Client 26] Continue +I [2026-10-13T05:03:22.856Z] [Client 26] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:22.856Z] [Client 26] OK application/ipp 247 +I [2026-10-13T05:03:22.856Z] [Client 26] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.856Z] [Client 26] Continue +I [2026-10-13T05:03:22.856Z] [Client 26] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:22.856Z] [Client 26] OK application/ipp 1033 +I [2026-10-13T05:03:22.856Z] [Client 26] Closing connection from 'localhost'. +I [2026-10-13T05:03:22.857Z] [Client 24] Closing connection from 'localhost'. +I [2026-10-13T05:03:22.871Z] [Client 27] Accepted connection from 'localhost'. +I [2026-10-13T05:03:22.871Z] [Client 27] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.871Z] [Client 27] Continue +I [2026-10-13T05:03:22.872Z] [Client 27] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:22.872Z] [Client 27] OK application/ipp 1033 +I [2026-10-13T05:03:22.873Z] [Client 27] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.873Z] [Client 27] Continue +I [2026-10-13T05:03:22.873Z] [Client 28] Accepted connection from 'localhost'. +I [2026-10-13T05:03:22.873Z] [Client 27] Validate-Job successful-ok +I [2026-10-13T05:03:22.873Z] [Client 27] OK application/ipp 75 +I [2026-10-13T05:03:22.873Z] [Client 28] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.873Z] [Client 28] Continue +I [2026-10-13T05:03:22.873Z] [Client 27] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.873Z] [Client 27] Continue +I [2026-10-13T05:03:22.873Z] [Client 28] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:22.873Z] [Client 28] OK application/ipp 1033 +I [2026-10-13T05:03:22.873Z] [Client 27] Create-Job successful-ok +I [2026-10-13T05:03:22.873Z] [Client 27] OK application/ipp 237 +I [2026-10-13T05:03:22.874Z] [Client 27] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.874Z] [Client 27] Continue +I [2026-10-13T05:03:22.874Z] [Job 4] Received 153 bytes of document data. +I [2026-10-13T05:03:22.874Z] [Client 27] Send-Document successful-ok +I [2026-10-13T05:03:22.874Z] [Job 4] Starting print job. +I [2026-10-13T05:03:22.874Z] [Client 27] OK application/ipp 229 +I [2026-10-13T05:03:22.874Z] [Job 4] PNG image dimensions are 76x180x1 +I [2026-10-13T05:03:22.874Z] [Job 4] PNG image resolution is 180x180dpi +I [2026-10-13T05:03:22.874Z] [Job 4] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:03:22.874Z] [Client 27] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.874Z] [Client 27] Continue +I [2026-10-13T05:03:22.874Z] [Client 27] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:22.874Z] [Client 27] OK application/ipp 1037 +I [2026-10-13T05:03:22.875Z] [Client 27] Closing connection from 'localhost'. +I [2026-10-13T05:03:22.875Z] [Client 28] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.875Z] [Client 28] Continue +I [2026-10-13T05:03:22.875Z] [Client 29] Accepted connection from 'localhost'. +I [2026-10-13T05:03:22.875Z] [Client 28] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:22.875Z] [Client 28] OK application/ipp 233 +I [2026-10-13T05:03:22.875Z] [Client 29] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:22.875Z] [Client 29] Continue +I [2026-10-13T05:03:22.875Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:22.875Z] [Client 29] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:22.875Z] [Client 29] OK application/ipp 233 +I [2026-10-13T05:03:22.878Z] [Job 4] Completed, job-impressions-completed=1. +I [2026-10-13T05:03:23.875Z] [Client 28] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.875Z] [Client 28] Continue +I [2026-10-13T05:03:23.875Z] [Client 28] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:23.875Z] [Client 28] OK application/ipp 1033 +I [2026-10-13T05:03:23.875Z] [Client 28] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.875Z] [Client 28] Continue +I [2026-10-13T05:03:23.875Z] [Client 28] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:23.875Z] [Client 28] OK application/ipp 247 +I [2026-10-13T05:03:23.876Z] [Client 29] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.876Z] [Client 29] Continue +I [2026-10-13T05:03:23.876Z] [Client 29] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:23.876Z] [Client 29] OK application/ipp 1033 +I [2026-10-13T05:03:23.876Z] [Client 29] Closing connection from 'localhost'. +I [2026-10-13T05:03:23.876Z] [Client 30] Accepted connection from 'localhost'. +I [2026-10-13T05:03:23.876Z] [Client 30] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.876Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:23.876Z] [Client 30] Continue +I [2026-10-13T05:03:23.876Z] [Client 30] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:23.876Z] [Client 30] OK application/ipp 247 +I [2026-10-13T05:03:23.876Z] [Client 30] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.877Z] [Client 30] Continue +I [2026-10-13T05:03:23.877Z] [Client 30] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:23.877Z] [Client 30] OK application/ipp 1033 +I [2026-10-13T05:03:23.877Z] [Client 30] Closing connection from 'localhost'. +I [2026-10-13T05:03:23.878Z] [Client 28] Closing connection from 'localhost'. +I [2026-10-13T05:03:23.898Z] [Client 31] Accepted connection from 'localhost'. +I [2026-10-13T05:03:23.898Z] [Client 31] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.898Z] [Client 31] Continue +I [2026-10-13T05:03:23.898Z] [Client 31] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:23.898Z] [Client 31] OK application/ipp 1033 +I [2026-10-13T05:03:23.899Z] [Client 31] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.899Z] [Client 31] Continue +I [2026-10-13T05:03:23.899Z] [Client 32] Accepted connection from 'localhost'. +I [2026-10-13T05:03:23.899Z] [Client 31] Validate-Job successful-ok +I [2026-10-13T05:03:23.899Z] [Client 31] OK application/ipp 75 +I [2026-10-13T05:03:23.899Z] [Client 32] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.899Z] [Client 32] Continue +I [2026-10-13T05:03:23.899Z] [Client 31] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.899Z] [Client 31] Continue +I [2026-10-13T05:03:23.899Z] [Client 31] Create-Job successful-ok +I [2026-10-13T05:03:23.899Z] [Client 32] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:23.899Z] [Client 31] OK application/ipp 237 +I [2026-10-13T05:03:23.899Z] [Client 32] OK application/ipp 1033 +I [2026-10-13T05:03:23.900Z] [Client 31] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.900Z] [Client 31] Continue +I [2026-10-13T05:03:23.900Z] [Client 32] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.900Z] [Client 32] Continue +I [2026-10-13T05:03:23.900Z] [Client 32] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:23.900Z] [Client 32] OK application/ipp 242 +I [2026-10-13T05:03:23.900Z] [Job 5] Received 2001 bytes of document data. +I [2026-10-13T05:03:23.900Z] [Client 31] Send-Document successful-ok +I [2026-10-13T05:03:23.900Z] [Client 31] OK application/ipp 220 +I [2026-10-13T05:03:23.900Z] [Job 5] Starting print job. +I [2026-10-13T05:03:23.900Z] [Client 31] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.900Z] [Client 31] Continue +I [2026-10-13T05:03:23.900Z] [Job 5] JPEG image dimensions are 76x180x1 +I [2026-10-13T05:03:23.900Z] [Client 31] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:23.900Z] [Client 31] OK application/ipp 1037 +I [2026-10-13T05:03:23.900Z] [Job 5] JPEG image resolution is 180x180dpi +I [2026-10-13T05:03:23.900Z] [Job 5] Auto-orientation: portrait +I [2026-10-13T05:03:23.901Z] [Client 31] Closing connection from 'localhost'. +I [2026-10-13T05:03:23.901Z] [Client 33] Accepted connection from 'localhost'. +I [2026-10-13T05:03:23.901Z] [Client 33] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:23.901Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:23.901Z] [Client 33] Continue +I [2026-10-13T05:03:23.901Z] [Client 33] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:23.901Z] [Client 33] OK application/ipp 233 +I [2026-10-13T05:03:23.902Z] [Job 5] Completed, job-impressions-completed=1. +I [2026-10-13T05:03:24.900Z] [Client 32] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.900Z] [Client 32] Continue +I [2026-10-13T05:03:24.900Z] [Client 32] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:24.900Z] [Client 32] OK application/ipp 1033 +I [2026-10-13T05:03:24.900Z] [Client 32] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.900Z] [Client 32] Continue +I [2026-10-13T05:03:24.900Z] [Client 32] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:24.900Z] [Client 32] OK application/ipp 247 +I [2026-10-13T05:03:24.902Z] [Client 33] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.902Z] [Client 33] Continue +I [2026-10-13T05:03:24.902Z] [Client 33] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:24.902Z] [Client 33] OK application/ipp 1033 +I [2026-10-13T05:03:24.902Z] [Client 33] Closing connection from 'localhost'. +I [2026-10-13T05:03:24.902Z] [Client 34] Accepted connection from 'localhost'. +I [2026-10-13T05:03:24.902Z] [Client 34] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.902Z] [Client 34] Continue +I [2026-10-13T05:03:24.902Z] [Client 34] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:24.902Z] [Client 34] OK application/ipp 247 +I [2026-10-13T05:03:24.902Z] [Client 34] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.902Z] [Client 34] Continue +I [2026-10-13T05:03:24.902Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:24.903Z] [Client 34] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:24.903Z] [Client 34] OK application/ipp 1033 +I [2026-10-13T05:03:24.903Z] [Client 34] Closing connection from 'localhost'. +I [2026-10-13T05:03:24.904Z] [Client 32] Closing connection from 'localhost'. +I [2026-10-13T05:03:24.978Z] [Client 35] Accepted connection from 'localhost'. +I [2026-10-13T05:03:24.978Z] [Client 35] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.978Z] [Client 35] Continue +I [2026-10-13T05:03:24.978Z] [Client 35] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:24.978Z] [Client 35] OK application/ipp 1033 +I [2026-10-13T05:03:24.980Z] [Client 35] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.980Z] [Client 35] Continue +I [2026-10-13T05:03:24.980Z] [Client 36] Accepted connection from 'localhost'. +I [2026-10-13T05:03:24.980Z] [Client 35] Validate-Job successful-ok +I [2026-10-13T05:03:24.980Z] [Client 35] OK application/ipp 75 +I [2026-10-13T05:03:24.980Z] [Client 36] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.980Z] [Client 36] Continue +I [2026-10-13T05:03:24.980Z] [Client 35] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.980Z] [Client 35] Continue +I [2026-10-13T05:03:24.980Z] [Client 36] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:24.980Z] [Client 36] OK application/ipp 1033 +I [2026-10-13T05:03:24.980Z] [Client 35] Create-Job successful-ok +I [2026-10-13T05:03:24.980Z] [Client 35] OK application/ipp 237 +I [2026-10-13T05:03:24.980Z] [Client 35] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.980Z] [Client 35] Continue +I [2026-10-13T05:03:24.981Z] [Client 36] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.981Z] [Client 36] Continue +I [2026-10-13T05:03:24.981Z] [Client 36] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:24.981Z] [Client 36] OK application/ipp 243 +I [2026-10-13T05:03:24.990Z] [Client 35] Receiving job file with 'gzip' compression. +I [2026-10-13T05:03:24.990Z] [Job 6] Starting print job. +I [2026-10-13T05:03:24.990Z] [Job 6] Page 1 raster data is 85x354x8 (sGray) +I [2026-10-13T05:03:24.993Z] [Job 6] Page 2 raster data is 85x354x8 (sGray) +I [2026-10-13T05:03:24.996Z] [Job 6] Completed, job-impressions-completed=2. +I [2026-10-13T05:03:24.996Z] [Client 35] Send-Document successful-ok +I [2026-10-13T05:03:24.996Z] [Client 35] OK application/ipp 244 +I [2026-10-13T05:03:24.997Z] [Client 35] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.997Z] [Client 35] Continue +I [2026-10-13T05:03:24.997Z] [Client 35] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:24.997Z] [Client 35] OK application/ipp 1033 +I [2026-10-13T05:03:24.997Z] [Client 35] Closing connection from 'localhost'. +I [2026-10-13T05:03:24.997Z] [Client 37] Accepted connection from 'localhost'. +I [2026-10-13T05:03:24.997Z] [Client 37] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.997Z] [Client 37] Continue +I [2026-10-13T05:03:24.997Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:24.997Z] [Client 37] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:24.997Z] [Client 37] OK application/ipp 248 +I [2026-10-13T05:03:24.998Z] [Client 37] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:24.998Z] [Client 37] Continue +I [2026-10-13T05:03:24.998Z] [Client 37] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:24.998Z] [Client 37] OK application/ipp 1033 +I [2026-10-13T05:03:24.998Z] [Client 37] Closing connection from 'localhost'. +I [2026-10-13T05:03:24.999Z] [Client 36] Closing connection from 'localhost'. +I [2026-10-13T05:03:25.091Z] [Client 38] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.091Z] [Client 38] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.091Z] [Client 38] Continue +I [2026-10-13T05:03:25.091Z] [Client 38] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.091Z] [Client 38] OK application/ipp 1033 +I [2026-10-13T05:03:25.093Z] [Client 38] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.093Z] [Client 38] Continue +I [2026-10-13T05:03:25.093Z] [Client 38] Validate-Job successful-ok +I [2026-10-13T05:03:25.093Z] [Client 38] OK application/ipp 75 +I [2026-10-13T05:03:25.093Z] [Client 39] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.093Z] [Client 38] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.093Z] [Client 38] Continue +I [2026-10-13T05:03:25.093Z] [Client 38] Create-Job successful-ok +I [2026-10-13T05:03:25.093Z] [Client 38] OK application/ipp 237 +I [2026-10-13T05:03:25.093Z] [Client 39] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.093Z] [Client 39] Continue +I [2026-10-13T05:03:25.094Z] [Client 39] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.094Z] [Client 38] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.094Z] [Client 38] Continue +I [2026-10-13T05:03:25.094Z] [Client 39] OK application/ipp 1033 +I [2026-10-13T05:03:25.094Z] [Client 39] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.094Z] [Client 39] Continue +I [2026-10-13T05:03:25.094Z] [Client 39] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:25.094Z] [Client 39] OK application/ipp 243 +I [2026-10-13T05:03:25.105Z] [Client 38] Receiving job file with 'gzip' compression. +I [2026-10-13T05:03:25.105Z] [Job 7] Starting print job. +I [2026-10-13T05:03:25.105Z] [Job 7] Page 1 raster data is 85x354x8 (sGray) +I [2026-10-13T05:03:25.108Z] [Job 7] Page 2 raster data is 85x354x8 (sGray) +I [2026-10-13T05:03:25.110Z] [Job 7] Completed, job-impressions-completed=2. +I [2026-10-13T05:03:25.110Z] [Client 38] Send-Document successful-ok +I [2026-10-13T05:03:25.110Z] [Client 38] OK application/ipp 244 +I [2026-10-13T05:03:25.110Z] [Client 38] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.110Z] [Client 38] Continue +I [2026-10-13T05:03:25.110Z] [Client 38] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.110Z] [Client 38] OK application/ipp 1033 +I [2026-10-13T05:03:25.111Z] [Client 38] Closing connection from 'localhost'. +I [2026-10-13T05:03:25.111Z] [Client 40] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.111Z] [Client 40] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.111Z] [Client 40] Continue +I [2026-10-13T05:03:25.111Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:25.111Z] [Client 40] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:25.111Z] [Client 40] OK application/ipp 248 +I [2026-10-13T05:03:25.111Z] [Client 40] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.111Z] [Client 40] Continue +I [2026-10-13T05:03:25.111Z] [Client 40] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.111Z] [Client 40] OK application/ipp 1033 +I [2026-10-13T05:03:25.112Z] [Client 40] Closing connection from 'localhost'. +I [2026-10-13T05:03:25.113Z] [Client 39] Closing connection from 'localhost'. +I [2026-10-13T05:03:25.181Z] [Client 41] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.182Z] [Client 41] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.182Z] [Client 41] Continue +I [2026-10-13T05:03:25.182Z] [Client 41] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.182Z] [Client 41] OK application/ipp 1033 +I [2026-10-13T05:03:25.183Z] [Client 41] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.183Z] [Client 41] Continue +I [2026-10-13T05:03:25.183Z] [Client 41] Validate-Job successful-ok +I [2026-10-13T05:03:25.183Z] [Client 41] OK application/ipp 75 +I [2026-10-13T05:03:25.183Z] [Client 42] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.183Z] [Client 42] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.183Z] [Client 41] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.183Z] [Client 41] Continue +I [2026-10-13T05:03:25.183Z] [Client 42] Continue +I [2026-10-13T05:03:25.183Z] [Client 41] Create-Job successful-ok +I [2026-10-13T05:03:25.183Z] [Client 41] OK application/ipp 237 +I [2026-10-13T05:03:25.183Z] [Client 42] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.183Z] [Client 42] OK application/ipp 1033 +I [2026-10-13T05:03:25.183Z] [Client 41] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.184Z] [Client 41] Continue +I [2026-10-13T05:03:25.184Z] [Client 42] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.184Z] [Client 42] Continue +I [2026-10-13T05:03:25.184Z] [Client 42] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:25.184Z] [Client 42] OK application/ipp 243 +I [2026-10-13T05:03:25.196Z] [Client 41] Receiving job file with 'gzip' compression. +I [2026-10-13T05:03:25.196Z] [Job 8] Starting print job. +I [2026-10-13T05:03:25.196Z] [Job 8] Page 1 raster data is 85x354x8 (sGray) +I [2026-10-13T05:03:25.199Z] [Job 8] Page 2 raster data is 85x354x8 (sGray) +I [2026-10-13T05:03:25.202Z] [Job 8] Page 3 raster data is 85x354x8 (sGray) +I [2026-10-13T05:03:25.205Z] [Job 8] Page 4 raster data is 85x354x8 (sGray) +I [2026-10-13T05:03:25.208Z] [Job 8] Completed, job-impressions-completed=4. +I [2026-10-13T05:03:25.208Z] [Client 41] Send-Document successful-ok +I [2026-10-13T05:03:25.208Z] [Client 41] OK application/ipp 244 +I [2026-10-13T05:03:25.208Z] [Client 41] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.208Z] [Client 41] Continue +I [2026-10-13T05:03:25.208Z] [Client 41] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.208Z] [Client 41] OK application/ipp 1033 +I [2026-10-13T05:03:25.208Z] [Client 41] Closing connection from 'localhost'. +I [2026-10-13T05:03:25.208Z] [Client 43] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.208Z] [Client 43] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.208Z] [Client 43] Continue +I [2026-10-13T05:03:25.208Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:25.209Z] [Client 43] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:25.209Z] [Client 43] OK application/ipp 248 +I [2026-10-13T05:03:25.209Z] [Client 43] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.209Z] [Client 43] Continue +I [2026-10-13T05:03:25.209Z] [Client 43] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.209Z] [Client 43] OK application/ipp 1033 +I [2026-10-13T05:03:25.209Z] [Client 43] Closing connection from 'localhost'. +I [2026-10-13T05:03:25.210Z] [Client 42] Closing connection from 'localhost'. +I [2026-10-13T05:03:25.225Z] [Client 44] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.225Z] [Client 44] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.225Z] [Client 44] Continue +I [2026-10-13T05:03:25.225Z] [Client 44] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.225Z] [Client 44] OK application/ipp 1033 +I [2026-10-13T05:03:25.226Z] [Client 44] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.226Z] [Client 44] Continue +I [2026-10-13T05:03:25.226Z] [Client 44] Validate-Job successful-ok +I [2026-10-13T05:03:25.226Z] [Client 44] OK application/ipp 75 +I [2026-10-13T05:03:25.226Z] [Client 45] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.226Z] [Client 45] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.226Z] [Client 45] Continue +I [2026-10-13T05:03:25.226Z] [Client 44] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.226Z] [Client 44] Continue +I [2026-10-13T05:03:25.226Z] [Client 44] Create-Job successful-ok +I [2026-10-13T05:03:25.226Z] [Client 44] OK application/ipp 237 +I [2026-10-13T05:03:25.226Z] [Client 45] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.226Z] [Client 45] OK application/ipp 1033 +I [2026-10-13T05:03:25.227Z] [Client 44] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.227Z] [Client 44] Continue +I [2026-10-13T05:03:25.227Z] [Client 45] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.227Z] [Client 45] Continue +I [2026-10-13T05:03:25.227Z] [Client 45] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:25.227Z] [Client 45] OK application/ipp 243 +I [2026-10-13T05:03:25.227Z] [Job 9] Received 136 bytes of document data. +I [2026-10-13T05:03:25.227Z] [Client 44] Send-Document successful-ok +I [2026-10-13T05:03:25.227Z] [Client 44] OK application/ipp 220 +I [2026-10-13T05:03:25.227Z] [Job 9] Starting print job. +I [2026-10-13T05:03:25.227Z] [Job 9] PNG image dimensions are 42x180x1 +I [2026-10-13T05:03:25.227Z] [Job 9] PNG image resolution is 180x180dpi +I [2026-10-13T05:03:25.227Z] [Client 44] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.227Z] [Client 44] Continue +I [2026-10-13T05:03:25.227Z] [Job 9] portrait image, 180 px along the tape at 180 dpi -> media length 3986 (1/100 mm), header.cupsHeight=282 PWG_ImageBox=[21 0 62 281] +I [2026-10-13T05:03:25.227Z] [Client 44] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:25.227Z] [Client 44] OK application/ipp 1037 +I [2026-10-13T05:03:25.228Z] [Client 44] Closing connection from 'localhost'. +I [2026-10-13T05:03:25.228Z] [Client 46] Accepted connection from 'localhost'. +I [2026-10-13T05:03:25.228Z] [Client 46] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:25.228Z] [Client 46] Continue +I [2026-10-13T05:03:25.228Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:25.228Z] [Client 46] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:25.228Z] [Client 46] OK application/ipp 234 +I [2026-10-13T05:03:25.230Z] [Job 9] Completed, job-impressions-completed=1. +I [2026-10-13T05:03:26.227Z] [Client 45] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.227Z] [Client 45] Continue +I [2026-10-13T05:03:26.227Z] [Client 45] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:26.227Z] [Client 45] OK application/ipp 1033 +I [2026-10-13T05:03:26.228Z] [Client 45] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.228Z] [Client 45] Continue +I [2026-10-13T05:03:26.228Z] [Client 45] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:26.228Z] [Client 45] OK application/ipp 248 +I [2026-10-13T05:03:26.228Z] [Client 46] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.228Z] [Client 46] Continue +I [2026-10-13T05:03:26.228Z] [Client 46] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:26.228Z] [Client 46] OK application/ipp 1033 +I [2026-10-13T05:03:26.228Z] [Client 46] Closing connection from 'localhost'. +I [2026-10-13T05:03:26.229Z] [Client 47] Accepted connection from 'localhost'. +I [2026-10-13T05:03:26.229Z] [Client 47] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.229Z] [Client 47] Continue +I [2026-10-13T05:03:26.229Z] [Client 47] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:26.229Z] [Client 47] OK application/ipp 248 +I [2026-10-13T05:03:26.229Z] [Client 47] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.229Z] [Client 47] Continue +I [2026-10-13T05:03:26.229Z] [Client 47] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:26.229Z] [Client 47] OK application/ipp 1033 +I [2026-10-13T05:03:26.229Z] [Client 47] Closing connection from 'localhost'. +I [2026-10-13T05:03:26.230Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:26.231Z] [Client 45] Closing connection from 'localhost'. +I [2026-10-13T05:03:26.263Z] [Client 48] Accepted connection from 'localhost'. +I [2026-10-13T05:03:26.263Z] [Client 48] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.263Z] [Client 48] Continue +I [2026-10-13T05:03:26.263Z] [Client 48] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:26.263Z] [Client 48] OK application/ipp 1033 +I [2026-10-13T05:03:26.264Z] [Client 48] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.264Z] [Client 48] Continue +I [2026-10-13T05:03:26.264Z] [Client 49] Accepted connection from 'localhost'. +I [2026-10-13T05:03:26.264Z] [Client 48] Validate-Job successful-ok +I [2026-10-13T05:03:26.264Z] [Client 48] OK application/ipp 75 +I [2026-10-13T05:03:26.264Z] [Client 49] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.264Z] [Client 49] Continue +I [2026-10-13T05:03:26.265Z] [Client 48] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.265Z] [Client 48] Continue +I [2026-10-13T05:03:26.265Z] [Client 49] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:26.265Z] [Client 49] OK application/ipp 1033 +I [2026-10-13T05:03:26.265Z] [Client 48] Create-Job successful-ok +I [2026-10-13T05:03:26.265Z] [Client 48] OK application/ipp 238 +I [2026-10-13T05:03:26.265Z] [Client 48] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.265Z] [Client 48] Continue +I [2026-10-13T05:03:26.265Z] [Job 10] Received 153 bytes of document data. +I [2026-10-13T05:03:26.265Z] [Client 48] Send-Document successful-ok +I [2026-10-13T05:03:26.265Z] [Client 48] OK application/ipp 221 +I [2026-10-13T05:03:26.265Z] [Job 10] Starting print job. +I [2026-10-13T05:03:26.265Z] [Job 10] PNG image dimensions are 76x180x1 +I [2026-10-13T05:03:26.265Z] [Job 10] PNG image resolution is 180x180dpi +I [2026-10-13T05:03:26.265Z] [Client 48] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.265Z] [Client 48] Continue +I [2026-10-13T05:03:26.265Z] [Job 10] portrait image, 180 px along the tape at 180 dpi -> media length 3986 (1/100 mm), header.cupsHeight=282 PWG_ImageBox=[21 0 62 281] +I [2026-10-13T05:03:26.265Z] [Client 48] Get-Printer-Attributes successful-ok +E [2026-10-13T05:03:26.265Z] [Job 10] PNG content is 76 dots across tape; printable width is 42 +I [2026-10-13T05:03:26.265Z] [Job 10] Aborted, job-impressions-completed=0. +I [2026-10-13T05:03:26.265Z] [Client 48] OK application/ipp 1037 +I [2026-10-13T05:03:26.266Z] [Client 49] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.266Z] [Client 49] Continue +I [2026-10-13T05:03:26.266Z] [Client 48] Closing connection from 'localhost'. +I [2026-10-13T05:03:26.266Z] [Client 49] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:26.266Z] [Client 49] OK application/ipp 247 +I [2026-10-13T05:03:26.266Z] [Client 50] Accepted connection from 'localhost'. +I [2026-10-13T05:03:26.266Z] [Client 50] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.266Z] [Client 50] Continue +I [2026-10-13T05:03:26.266Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:26.266Z] [Client 50] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:26.266Z] [Client 50] OK application/ipp 247 +I [2026-10-13T05:03:26.266Z] [Client 50] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.266Z] [Client 50] Continue +I [2026-10-13T05:03:26.266Z] [Client 50] Get-Printer-Attributes successful-ok +I [2026-10-13T05:03:26.266Z] [Client 50] OK application/ipp 1033 +I [2026-10-13T05:03:26.267Z] [Client 50] Closing connection from 'localhost'. +I [2026-10-13T05:03:26.268Z] [Client 49] Closing connection from 'localhost'. +I [2026-10-13T05:03:26.272Z] [Client 51] Accepted connection from 'localhost'. +I [2026-10-13T05:03:26.272Z] [Client 51] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.272Z] [Client 51] Continue +I [2026-10-13T05:03:26.273Z] [Job 11] Received 153 bytes of document data. +I [2026-10-13T05:03:26.273Z] [Client 51] Print-Job successful-ok +I [2026-10-13T05:03:26.273Z] [Client 51] OK application/ipp 218 +I [2026-10-13T05:03:26.273Z] [Job 11] Starting print job. +I [2026-10-13T05:03:26.273Z] [Job 11] PNG image dimensions are 76x180x1 +I [2026-10-13T05:03:26.273Z] [Job 11] PNG image resolution is 180x180dpi +I [2026-10-13T05:03:26.273Z] [Job 11] portrait image, 180 px along the tape at 180 dpi -> media length 4000 (1/100 mm), header.cupsHeight=283 PWG_ImageBox=[4 0 79 282] +I [2026-10-13T05:03:26.273Z] [Client 51] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:26.273Z] [Client 51] Continue +I [2026-10-13T05:03:26.273Z] [Client 51] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:26.273Z] [Client 51] OK application/ipp 878 +I [2026-10-13T05:03:26.275Z] [Job 11] Completed, job-impressions-completed=1. +I [2026-10-13T05:03:31.275Z] [Client 51] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:31.275Z] [Client 51] Continue +I [2026-10-13T05:03:31.275Z] [Client 51] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:31.275Z] [Client 51] OK application/ipp 908 +I [2026-10-13T05:03:31.277Z] [Client 51] Closing connection from 'localhost'. +I [2026-10-13T05:03:31.283Z] [Client 52] Accepted connection from 'localhost'. +I [2026-10-13T05:03:31.283Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:31.284Z] [Client 52] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:31.284Z] [Client 52] Continue +I [2026-10-13T05:03:31.284Z] [Job 12] Received 153 bytes of document data. +I [2026-10-13T05:03:31.284Z] [Client 52] Print-Job successful-ok +I [2026-10-13T05:03:31.284Z] [Client 52] OK application/ipp 218 +I [2026-10-13T05:03:31.284Z] [Job 12] Starting print job. +I [2026-10-13T05:03:31.284Z] [Job 12] PNG image dimensions are 76x180x1 +I [2026-10-13T05:03:31.284Z] [Job 12] PNG image resolution is 180x180dpi +I [2026-10-13T05:03:31.284Z] [Job 12] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:03:31.284Z] [Client 52] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:31.284Z] [Client 52] Continue +I [2026-10-13T05:03:31.284Z] [Client 52] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:31.284Z] [Client 52] OK application/ipp 828 +I [2026-10-13T05:03:31.286Z] [Job 12] Completed, job-impressions-completed=1. +I [2026-10-13T05:03:36.286Z] [Client 52] HTTP_STATE_POST http://localhost:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:03:36.286Z] [Client 52] Continue +I [2026-10-13T05:03:36.286Z] [Client 52] Get-Job-Attributes successful-ok +I [2026-10-13T05:03:36.286Z] [Client 52] OK application/ipp 858 +I [2026-10-13T05:03:36.287Z] [Client 52] Closing connection from 'localhost'. +I [2026-10-13T05:03:36.320Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:03:36.320Z] Shutting down system. + + diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-before-driver.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-before-driver.txt new file mode 100644 index 0000000..e848834 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-before-driver.txt @@ -0,0 +1,13 @@ +Internal ctest changing into directory: /results/build +Test project /results/build + Start 1: driver_finishings +1/1 Test #1: driver_finishings ................Subprocess aborted***Exception: 1.32 sec +test_driver: /src/tests/test_driver.c:62: dev_write: Assertion `papplPrinterGetReasons(poll_during_job) == before && usb_reads == reads' failed. + + +0% tests passed, 1 tests failed out of 1 + +Total Test time (real) = 1.32 sec + +The following tests FAILED: + 1 - driver_finishings (Subprocess aborted) diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-before.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-before.txt new file mode 100644 index 0000000..38cfe49 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-before.txt @@ -0,0 +1,19 @@ +Internal ctest changing into directory: /results/build +Test project /results/build + Start 1: driver_finishings + Start 4: png_geometry +1/2 Test #4: png_geometry .....................Subprocess aborted***Exception: 0.22 sec +PNG media7: got 180 raster rows, expected 354 +test_png_geometry: /src/tests/test_png_geometry.c:190: run_image: Assertion `actual_rows == expected_rows' failed. + +2/2 Test #1: driver_finishings ................Subprocess aborted***Exception: 11.21 sec +test_driver: /src/tests/test_driver.c:781: main: Assertion `busy_poll_checked && papplJobGetState(busy_job) == IPP_JSTATE_COMPLETED' failed. + + +0% tests passed, 2 tests failed out of 2 + +Total Test time (real) = 11.21 sec + +The following tests FAILED: + 1 - driver_finishings (Subprocess aborted) + 4 - png_geometry (Subprocess aborted) diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-final.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-final.txt new file mode 100644 index 0000000..216b0a8 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-final.txt @@ -0,0 +1,10 @@ +Internal ctest changing into directory: /results/final +Test project /results/final + Start 1: driver_finishings + Start 4: png_geometry +1/2 Test #1: driver_finishings ................ Passed 29.50 sec +2/2 Test #4: png_geometry ..................... Passed 29.50 sec + +100% tests passed, 0 tests failed out of 2 + +Total Test time (real) = 29.51 sec diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-report.md b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-report.md new file mode 100644 index 0000000..647446e --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/driver-png-report.md @@ -0,0 +1,28 @@ +Scoped review fixes, 2026-10-13 + +Changed production paths: src/driver.c and src/filter_png.c. +Changed regression paths: tests/test_driver.c and tests/test_png_geometry.c. +No printing, remote access, deployment, or dependency changes. + +Reproductions: +- before.log: collection media-size-name=om_12x50mm_12x50mm produced180 rows instead of354. +- before-driver.log: real scheduled USB-fixture PNG job polled status while processing; polling changed printer reasons. The run is intentionally UNCERTAIN during partial output, so the old recovery check published OTHER before reaching device acquisition. + +Fixes: +- PNG explicit-length detection recognizes resolved media-col.media-size-name, matching PAPPL's name precedence. +- Status callback skips idle polling/recovery checks during processing and rechecks processing when OpenDevice returns NULL after an idle-to-active scheduling race. Idle present-but-unclaimable USB regression still requires OFFLINE/unconfirmed. +- Bounded custom-width support advertises only canonical±1 centimm in five existing variable-length size ranges. Named preset sizes remain exact. PNG tape lookup rounds millimeters; noncanonical raster width is permitted only when matching the explicitly selected ±1 dimensions and retaining the physical printable window. Explicit margins remain unchanged. The page span uses the actual header width, and PNG rejection reports the selected printable limit (42dots for the1199centimm/297margin case). + +Evidence: +- Actual CUPS headers:1198/1199 centimm ->84dots,1200/1201/1202 ->85dots; all11bytes/row. +- Named50mm collection produces354rows; named25mm too short and named18mm wrong tape abort with no raster/finishing commands. +- Custom1199/1201 x3986centimm with explicit297 margins and42dot PNG produces282rows.76dot PNG rejects with no raster/finishing commands. +- Validate-Job with fidelity=true: canonical±1accepted,±2unsupported, for6/9/12/18/24mm. +- Active-job poll preserves media confirmation and reasons and emits no status reads; job completes. Existing idle USB claim-failure, unsupported width, malformed raster, canceled/faulted run,65/999copy and PNG geometry cases remain covered. + +Verification command inside ptouch-named-media-build:20261013 isolated with network none and repository read-only: +cmake -S /src -B /results/final -DCMAKE_BUILD_TYPE=Release +cmake --build /results/final --target test_driver test_png_geometry -j4 +ctest --test-dir /results/final -R 'driver_finishings|png_geometry' --output-on-failure -j2 + +Result:2/2passed; driver_finishings29.50s,png_geometry29.50s,total29.51s. build-final.log contains no compiler warnings. final.log is the authoritative final output. diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-deploy.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-deploy.txt new file mode 100644 index 0000000..1eaffb2 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-deploy.txt @@ -0,0 +1,387 @@ +#0 building with "default" instance using docker driver + +#1 [internal] load build definition from Dockerfile.deploy +#1 transferring dockerfile: 2.76kB done +#1 DONE 0.0s + +#2 resolve image config for docker-image://docker.io/docker/dockerfile:1 +#2 DONE 0.4s + +#3 docker-image://docker.io/docker/dockerfile:1@sha256:4edf897a3ffa55b89f906fc8cc78afdb3f1834cc9c7083565e611a8a7d5fe99e +#3 resolve docker.io/docker/dockerfile:1@sha256:4edf897a3ffa55b89f906fc8cc78afdb3f1834cc9c7083565e611a8a7d5fe99e 0.1s done +#3 CACHED + +#4 [internal] load metadata for docker.io/library/debian:bookworm +#4 ... + +#5 [internal] load metadata for docker.io/library/debian:bookworm-slim +#5 DONE 0.3s + +#4 [internal] load metadata for docker.io/library/debian:bookworm +#4 DONE 0.3s + +#6 [internal] load .dockerignore +#6 transferring context: 79B done +#6 DONE 0.0s + +#7 [runtime 1/13] FROM docker.io/library/debian:bookworm-slim@sha256:7c7b2c966bc9ee8cedfeef67e0e279108992c77681fa595db4a9d65c06ccc587 +#7 resolve docker.io/library/debian:bookworm-slim@sha256:7c7b2c966bc9ee8cedfeef67e0e279108992c77681fa595db4a9d65c06ccc587 0.0s done +#7 DONE 0.0s + +#8 [build 1/11] FROM docker.io/library/debian:bookworm@sha256:2c037a04925515fdd6ea85ea14a682d0e79931f5e9f5d07b6dbfc6ba12f9e858 +#8 resolve docker.io/library/debian:bookworm@sha256:2c037a04925515fdd6ea85ea14a682d0e79931f5e9f5d07b6dbfc6ba12f9e858 0.0s done +#8 DONE 0.0s + +#9 [internal] load build context +#9 transferring context: 745.56kB 0.0s done +#9 DONE 0.0s + +#10 [build 2/11] RUN apt-get update && apt-get install -y --no-install-recommends build-essential cmake pkg-config git ca-certificates python3 libusb-1.0-0-dev libpng-dev libjpeg-dev zlib1g-dev libcups2-dev libcupsimage2-dev libssl-dev libavahi-client-dev libgnutls28-dev libkrb5-dev && rm -rf /var/lib/apt/lists/* +#10 CACHED + +#11 [build 3/11] COPY patches/cups-2.4.2-deb12u9-raster.patch /tmp/cups-raster.patch +#11 CACHED + +#12 [build 4/11] COPY scripts/build-cups.sh /tmp/build-cups.sh +#12 CACHED + +#13 [build 5/11] RUN /tmp/build-cups.sh /tmp/cups-raster.patch +#13 CACHED + +#14 [build 6/11] COPY patches/pappl-1.4.11-raster.patch /tmp/pappl-raster.patch +#14 DONE 0.1s + +#15 [build 7/11] COPY patches/pappl-1.4.11-finishings.patch /tmp/pappl-finishings.patch +#15 DONE 0.1s + +#16 [build 8/11] RUN git clone --depth 1 --branch v1.4.11 https://github.com/michaelrsweet/pappl /tmp/pappl && cd /tmp/pappl && patch --batch --forward --fuzz=0 -p1 < /tmp/pappl-raster.patch && patch --batch --forward --fuzz=0 -p1 < /tmp/pappl-finishings.patch && ./configure && make && make install && ldconfig && rm -rf /tmp/pappl +#16 0.200 Cloning into '/tmp/pappl'... +#16 1.321 Note: switching to 'ad86a0a8473f0f83233a374226561181570d4f81'. +#16 1.321 +#16 1.321 You are in 'detached HEAD' state. You can look around, make experimental +#16 1.321 changes and commit them, and you can discard any commits you make in this +#16 1.321 state without impacting any branches by switching back to a branch. +#16 1.321 +#16 1.321 If you want to create a new branch to retain commits you create, you may +#16 1.321 do so (now or later) by using -c with the switch command. Example: +#16 1.321 +#16 1.321 git switch -c +#16 1.321 +#16 1.321 Or undo this operation with: +#16 1.321 +#16 1.321 git switch - +#16 1.321 +#16 1.321 Turn off this advice by setting config variable advice.detachedHead to false +#16 1.321 +#16 1.356 patching file pappl/job-process.c +#16 1.356 patching file pappl/job-filter.c +#16 1.358 patching file pappl/printer-driver.c +#16 1.358 patching file pappl/printer-ipp.c +#16 1.359 patching file pappl/job-process.c +#16 1.359 patching file pappl/finishings-private.h +#16 1.359 patching file pappl/system-loadsave.c +#16 1.359 patching file pappl/printer-webif.c +#16 1.456 checking build system type... x86_64-pc-linux-gnu +#16 1.542 checking host system type... x86_64-pc-linux-gnu +#16 1.546 checking for gcc... gcc +#16 1.575 checking whether the C compiler works... yes +#16 1.620 checking for C compiler default output file name... a.out +#16 1.621 checking for suffix of executables... +#16 1.656 checking whether we are cross compiling... no +#16 1.700 checking for suffix of object files... o +#16 1.722 checking whether the compiler supports GNU C... yes +#16 1.741 checking whether gcc accepts -g... yes +#16 1.761 checking for gcc option to enable C11 features... none needed +#16 1.791 checking for ranlib... ranlib +#16 1.792 checking for ar... /usr/bin/ar +#16 1.792 checking for codesign... no +#16 1.792 checking for true... /usr/bin/true +#16 1.793 checking for mkdir... /usr/bin/mkdir +#16 1.793 checking for rm... /usr/bin/rm +#16 1.793 checking for rmdir... /usr/bin/rmdir +#16 1.793 checking for ln... /usr/bin/ln +#16 1.794 checking for install-sh script... using /tmp/pappl/install-sh +#16 1.795 checking for pkg-config... /usr/bin/pkg-config +#16 1.795 checking for cups-config... /usr/bin/cups-config +#16 1.795 checking for CUPS library v2.2 or higher... yes, v2.4 +#16 2.148 checking for strlcpy... no +#16 2.204 checking for stdio.h... yes +#16 2.223 checking for stdlib.h... yes +#16 2.260 checking for string.h... yes +#16 2.284 checking for inttypes.h... yes +#16 2.309 checking for stdint.h... yes +#16 2.335 checking for strings.h... yes +#16 2.360 checking for sys/stat.h... yes +#16 2.387 checking for sys/types.h... yes +#16 2.413 checking for unistd.h... yes +#16 2.447 checking for pthread.h... yes +#16 2.482 checking for pthread_create using -lpthreads... no +#16 2.525 checking for pthread_create using -lpthread... yes +#16 2.571 checking for Avahi... yes +#16 2.577 checking for OpenSSL/LibreSSL package... yes +#16 2.584 checking for sys/random.h... yes +#16 2.623 checking for arc4random... yes +#16 2.679 checking for getrandom... yes +#16 2.726 checking for gnutls_rnd... no +#16 2.777 checking for sys/mount.h... yes +#16 2.817 checking for sys/statfs.h... yes +#16 2.847 checking for sys/statvfs.h... yes +#16 2.878 checking for sys/vfs.h... yes +#16 2.908 checking for statfs... yes +#16 2.957 checking for statvfs... yes +#16 3.006 checking for libjpeg via pkg-config... yes +#16 3.013 checking for libpng-1.6.x... yes +#16 3.020 checking for libusb-1.0... yes +#16 3.026 checking for dlopen in -ldl... yes +#16 3.079 checking for pam_start in -lpam... no +#16 3.121 checking for security/pam_appl.h... no +#16 3.154 checking for pam/pam_appl.h... no +#16 3.185 checking whether compiler supports -Wno-char-subscripts... yes +#16 3.206 checking whether compiler supports -Wno-format-truncation... yes +#16 3.226 checking whether compiler supports -Wno-format-y2k... yes +#16 3.250 checking whether compiler supports -Wno-switch... yes +#16 3.270 checking whether compiler supports -Wno-unused-result... yes +#16 3.290 checking whether compiler supports -fPIE... yes +#16 3.312 checking for OS-specific compiler options... -D__USE_MISC -D_GNU_SOURCE -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -Wl,-z,relro,-z,now +#16 3.328 configure: creating ./config.status +#16 3.417 config.status: creating Makedefs +#16 3.428 config.status: creating pappl/pappl.pc +#16 3.447 config.status: creating config.h +#16 3.476 Using CC=gcc +#16 3.477 Using CFLAGS=-fPIC -I.. -D_THREAD_SAFE -D_REENTRANT -D_REENTRANT -I/usr/include/libpng16 -I/usr/include/libusb-1.0 -D_FORTIFY_SOURCE=2 -D__USE_MISC -D_GNU_SOURCE -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -g -Os -Wall -Wunused -Wno-char-subscripts -Wno-format-truncation -Wno-format-y2k -Wno-switch -Wno-unused-result +#16 3.478 Using DSOFLAGS=-fPIC -I.. -D_THREAD_SAFE -D_REENTRANT -D_REENTRANT -I/usr/include/libpng16 -I/usr/include/libusb-1.0 -D_FORTIFY_SOURCE=2 -D__USE_MISC -D_GNU_SOURCE -D_TIME_BITS=64 -D_FILE_OFFSET_BITS=64 -g -Os -Wall -Wunused -Wno-char-subscripts -Wno-format-truncation -Wno-format-y2k -Wno-switch -Wno-unused-result +#16 3.479 Using LDFLAGS=-fPIE -pie -Wl,-z,relro,-z,now -g -Os +#16 3.480 Using LIBS=-ldl -lpthread -lcups -lavahi-common -lavahi-client -lssl -lcrypto -ljpeg -lpng16 -lz -lusb-1.0 +#16 3.480 Making all in doc... +#16 3.482 Making all in man... +#16 3.485 Making all in pappl... +#16 3.487 make[1]: Entering directory '/tmp/pappl/pappl' +#16 3.488 Compiling client.c... +#16 3.737 Compiling client-accessors.c... +#16 3.851 Compiling client-auth.c... +#16 3.948 Compiling client-loc.c... +#16 4.047 Compiling client-ipp.c... +#16 4.187 Compiling client-webif.c... +#16 4.506 Compiling contact.c... +#16 4.589 Compiling device.c... +#16 4.779 Compiling device-file.c... +#16 4.907 Compiling device-network.c... +#16 5.260 Compiling device-usb.c... +#16 5.454 Compiling dnssd.c... +#16 5.676 Compiling httpmon.c... +#16 5.824 Compiling job-accessors.c... +#16 5.995 Compiling job-filter.c... +#16 6.240 Compiling job-ipp.c... +#16 6.434 Compiling job-process.c... +#16 6.726 Compiling job.c... +#16 6.977 Compiling link.c... +#16 7.074 Compiling loc.c... +#16 7.274 Compiling log.c... +#16 7.471 Compiling lookup.c... +#16 7.548 Compiling mainloop.c... +#16 7.696 Compiling mainloop-subcommands.c... +#16 8.063 Compiling mainloop-support.c... +#16 8.274 Compiling printer.c... +#16 8.474 Compiling printer-accessors.c... +#16 8.707 Compiling printer-driver.c... +#16 9.203 Compiling printer-ipp.c... +#16 9.804 Compiling printer-raw.c... +#16 9.929 Compiling printer-support.c... +#16 10.07 Compiling printer-usb.c... +#16 10.38 Compiling printer-webif.c... +#16 10.85 Compiling resource.c... +#16 10.99 Compiling snmp.c... +#16 11.21 Compiling subscription.c... +#16 11.33 Compiling subscription-ipp.c... +#16 11.50 Compiling system.c... +#16 11.81 Compiling system-accessors.c... +#16 12.22 Compiling system-ipp.c... +#16 12.49 Compiling system-loadsave.c... +#16 12.83 Compiling system-loc.c... +#16 12.91 Compiling system-printer.c... +#16 13.02 Compiling system-subscription.c... +#16 13.15 Compiling system-webif.c... +#16 13.78 Compiling util.c... +#16 13.89 Linking libpappl.so.1... +#16 13.98 Archiving libpappl.a... +#16 14.04 make[1]: Leaving directory '/tmp/pappl/pappl' +#16 14.04 Making all in testsuite... +#16 14.04 make[1]: Entering directory '/tmp/pappl/testsuite' +#16 14.04 Compiling testhttpmon.c... +#16 14.14 Linking testhttpmon... +#16 14.17 Compiling testmainloop.c... +#16 14.25 Compiling pwg-driver.c... +#16 14.47 Linking testmainloop... +#16 14.60 Compiling testpappl.c... +#16 15.39 Linking testpappl... +#16 15.51 make[1]: Leaving directory '/tmp/pappl/testsuite' +#16 15.52 Installing in doc... +#16 15.52 make[1]: Entering directory '/tmp/pappl/doc' +#16 15.52 Installing documentation in //usr/local/share/doc/pappl... +#16 15.56 make[1]: Leaving directory '/tmp/pappl/doc' +#16 15.56 Installing in man... +#16 15.56 make[1]: Entering directory '/tmp/pappl/man' +#16 15.56 Installing man pages in //usr/local/share/man/man1... +#16 15.58 Installing man pages in //usr/local/share/man/man3... +#16 15.66 make[1]: Leaving directory '/tmp/pappl/man' +#16 15.66 Installing in pappl... +#16 15.67 make[1]: Entering directory '/tmp/pappl/pappl' +#16 15.67 Installing pappl-makeresheader in /usr/local/bin... +#16 15.68 Installing libpappl in /usr/local/lib... +#16 15.71 Installing pkg-config files in /usr/local/lib/pkgconfig... +#16 15.72 Installing data files in /usr/local/share/pappl... +#16 15.76 Installing header files in /usr/local/include/pappl... +#16 15.86 make[1]: Leaving directory '/tmp/pappl/pappl' +#16 15.86 Installing in testsuite... +#16 DONE 16.1s + +#17 [build 9/11] WORKDIR /src +#17 DONE 0.1s + +#18 [build 10/11] COPY . . +#18 DONE 0.0s + +#19 [build 11/11] RUN cmake -S . -B build-app -DPTOUCH_CORE_ONLY=OFF && cmake --build build-app && ctest --test-dir build-app --output-on-failure && install -Dm755 build-app/ptouch-app /out/usr/local/bin/ptouch-app +#19 0.252 -- The C compiler identification is GNU 12.2.0 +#19 0.266 -- Detecting C compiler ABI info +#19 0.347 -- Detecting C compiler ABI info - done +#19 0.355 -- Check for working C compiler: /usr/bin/cc - skipped +#19 0.356 -- Detecting C compile features +#19 0.357 -- Detecting C compile features - done +#19 0.455 -- Found Python3: /usr/bin/python3 (found version "3.11.2") found components: Interpreter +#19 0.461 -- Found PkgConfig: /usr/bin/pkg-config (found version "1.8.1") +#19 0.461 -- Checking for module 'pappl' +#19 0.474 -- Found pappl, version 1.4.11 +#19 0.509 -- Checking for module 'libusb-1.0' +#19 0.521 -- Found libusb-1.0, version 1.0.26 +#19 0.553 -- Checking for module 'libpng' +#19 0.565 -- Found libpng, version 1.6.39 +#19 0.598 -- Configuring done +#19 0.619 -- Generating done +#19 0.622 -- Build files have been written to: /src/build-app +#19 0.681 [ 2%] Building C object CMakeFiles/ptcore.dir/src/tables.c.o +#19 0.716 [ 5%] Building C object CMakeFiles/ptcore.dir/src/protocol.c.o +#19 0.752 [ 8%] Building C object CMakeFiles/ptcore.dir/src/raster.c.o +#19 0.785 [ 10%] Building C object CMakeFiles/ptcore.dir/src/status.c.o +#19 0.816 [ 13%] Building C object CMakeFiles/ptcore.dir/src/cutter.c.o +#19 0.844 [ 16%] Linking C static library libptcore.a +#19 0.885 [ 16%] Built target ptcore +#19 0.909 [ 18%] Building C object CMakeFiles/test_driver.dir/tests/test_driver.c.o +#19 1.164 [ 21%] Building C object CMakeFiles/test_driver.dir/src/filter_png.c.o +#19 1.248 [ 24%] Linking C executable test_driver +#19 1.333 [ 24%] Built target test_driver +#19 1.357 [ 27%] Building C object CMakeFiles/test_pappl_raster.dir/tests/test_pappl_raster.c.o +#19 1.455 [ 29%] Linking C executable test_pappl_raster +#19 1.539 [ 29%] Built target test_pappl_raster +#19 1.562 [ 32%] Building C object CMakeFiles/test_pappl_finishings.dir/tests/test_pappl_finishings.c.o +#19 1.650 [ 35%] Linking C executable test_pappl_finishings +#19 1.733 [ 35%] Built target test_pappl_finishings +#19 1.758 [ 37%] Building C object CMakeFiles/test_png_geometry.dir/tests/test_png_geometry.c.o +#19 1.864 [ 40%] Building C object CMakeFiles/test_png_geometry.dir/src/driver.c.o +#19 1.972 [ 43%] Building C object CMakeFiles/test_png_geometry.dir/src/filter_png.c.o +#19 2.055 [ 45%] Linking C executable test_png_geometry +#19 2.152 [ 45%] Built target test_png_geometry +#19 2.175 [ 48%] Building C object CMakeFiles/test_device_usb.dir/tests/test_device_usb.c.o +#19 2.265 [ 51%] Linking C executable test_device_usb +#19 2.345 [ 51%] Built target test_device_usb +#19 2.368 [ 54%] Building C object CMakeFiles/ptouch-app.dir/src/main.c.o +#19 2.431 [ 56%] Building C object CMakeFiles/ptouch-app.dir/src/driver.c.o +#19 2.534 [ 59%] Building C object CMakeFiles/ptouch-app.dir/src/device_usb.c.o +#19 2.605 [ 62%] Building C object CMakeFiles/ptouch-app.dir/src/filter_png.c.o +#19 2.688 [ 64%] Linking C executable ptouch-app +#19 2.771 [ 64%] Built target ptouch-app +#19 2.794 [ 67%] Building C object tests/CMakeFiles/test_tables.dir/test_tables.c.o +#19 2.828 [ 70%] Linking C executable test_tables +#19 2.868 [ 70%] Built target test_tables +#19 2.892 [ 72%] Building C object tests/CMakeFiles/test_protocol.dir/test_protocol.c.o +#19 2.929 [ 75%] Linking C executable test_protocol +#19 2.969 [ 75%] Built target test_protocol +#19 2.994 [ 78%] Building C object tests/CMakeFiles/test_raster.dir/test_raster.c.o +#19 3.031 [ 81%] Linking C executable test_raster +#19 3.071 [ 81%] Built target test_raster +#19 3.095 [ 83%] Building C object tests/CMakeFiles/test_status.dir/test_status.c.o +#19 3.132 [ 86%] Linking C executable test_status +#19 3.175 [ 86%] Built target test_status +#19 3.200 [ 89%] Building C object tests/CMakeFiles/test_cutter.dir/test_cutter.c.o +#19 3.231 [ 91%] Linking C executable test_cutter +#19 3.270 [ 91%] Built target test_cutter +#19 3.294 [ 94%] Building C object tests/CMakeFiles/ptouch-cut-probe.dir/cut_probe.c.o +#19 3.376 [ 97%] Building C object tests/CMakeFiles/ptouch-cut-probe.dir/__/src/device_usb.c.o +#19 3.450 [100%] Linking C executable ptouch-cut-probe +#19 3.534 [100%] Built target ptouch-cut-probe +#19 3.590 Internal ctest changing into directory: /src/build-app +#19 3.590 Test project /src/build-app +#19 3.591 Start 1: driver_finishings +#19 33.45 1/11 Test #1: driver_finishings ................ Passed 29.86 sec +#19 33.45 Start 2: pappl_raster +#19 63.45 2/11 Test #2: pappl_raster ..................... Passed 30.00 sec +#19 63.45 Start 3: pappl_finishings +#19 93.42 3/11 Test #3: pappl_finishings ................. Passed 29.97 sec +#19 93.42 Start 4: png_geometry +#19 123.4 4/11 Test #4: png_geometry ..................... Passed 30.03 sec +#19 123.4 Start 5: device_usb +#19 123.5 5/11 Test #5: device_usb ....................... Passed 0.01 sec +#19 123.5 Start 6: tables +#19 123.5 6/11 Test #6: tables ........................... Passed 0.00 sec +#19 123.5 Start 7: protocol +#19 123.5 7/11 Test #7: protocol ......................... Passed 0.00 sec +#19 123.5 Start 8: raster +#19 123.5 8/11 Test #8: raster ........................... Passed 0.00 sec +#19 123.5 Start 9: status +#19 123.5 9/11 Test #9: status ........................... Passed 0.00 sec +#19 123.5 Start 10: cutter +#19 123.5 10/11 Test #10: cutter ........................... Passed 0.00 sec +#19 123.5 Start 11: cut_probe_capture +#19 124.1 11/11 Test #11: cut_probe_capture ................ Passed 0.63 sec +#19 124.1 +#19 124.1 100% tests passed, 0 tests failed out of 11 +#19 124.1 +#19 124.1 Total Test time (real) = 120.50 sec +#19 DONE 124.1s + +#20 [runtime 3/13] COPY --from=build /usr/local/lib/libcups.so* /usr/local/lib/ +#20 CACHED + +#21 [runtime 4/13] COPY --from=build /usr/local/lib/libcupsimage.so* /usr/local/lib/ +#21 CACHED + +#22 [runtime 5/13] COPY --from=build /usr/local/share/ptouch/cups-source-version /usr/local/share/ptouch/cups-source-version +#22 CACHED + +#23 [runtime 2/13] RUN apt-get update && apt-get install -y --no-install-recommends libusb-1.0-0 libcups2 libcupsimage2 libavahi-client3 libpng16-16 libjpeg62-turbo zlib1g libssl3 avahi-daemon dbus ca-certificates && rm -rf /var/lib/apt/lists/* +#23 CACHED + +#24 [runtime 6/13] RUN test "$(dpkg-query -W -f='${source:Version}' libcups2)" = "$(cat /usr/local/share/ptouch/cups-source-version)" +#24 CACHED + +#25 [runtime 7/13] COPY --from=build /usr/local/lib/libpappl.so* /usr/local/lib/ +#25 DONE 0.1s + +#26 [runtime 8/13] COPY --from=build /out/usr/local/bin/ptouch-app /usr/local/bin/ptouch-app +#26 DONE 0.0s + +#27 [runtime 9/13] RUN ldconfig +#27 DONE 0.3s + +#28 [runtime 10/13] RUN sh -c 'ldd /usr/local/bin/ptouch-app | grep -i "not found" && exit 1 || true' +#28 DONE 0.2s + +#29 [runtime 11/13] COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +#29 DONE 0.0s + +#30 [runtime 12/13] COPY THIRD_PARTY_NOTICES.md /THIRD_PARTY_NOTICES.md +#30 DONE 0.0s + +#31 [runtime 13/13] RUN chmod +x /usr/local/bin/docker-entrypoint.sh +#31 DONE 0.2s + +#32 exporting to image +#32 exporting layers +#32 exporting layers 0.3s done +#32 exporting manifest sha256:c9d5f1fc82d28d47f7d7f36fed4bb6d2cf84dd3b2653a1d879dbe3a3c94e9470 0.0s done +#32 exporting config sha256:a448078c482a1f6019af942caf771a5e85a71e7eaf67e22a13655df3cf78d26f done +#32 exporting attestation manifest sha256:19a22f552aa2922debf88f82b99ca002d564f7da59438207350d759c654bfb32 0.0s done +#32 exporting manifest list sha256:babe9d5a3dfdd36d0602e4e76d33a658f754b77298664df30ef055bd7adf3308 0.0s done +#32 naming to docker.io/library/ptouch-review-fixed-deploy:20261013 done +#32 unpacking to docker.io/library/ptouch-review-fixed-deploy:20261013 0.1s done +#32 DONE 0.5s diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-openspec.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-openspec.txt new file mode 100644 index 0000000..8ac31a0 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-openspec.txt @@ -0,0 +1,6 @@ +- Validating... +✓ spec/batch-printing + ℹ [INFO] requirements[1]: Requirement text is very long (>500 characters). Move examples and edge cases into scenarios, or split it into separate requirements that each state one behavior. +✓ spec/label-rendering +✓ spec/printer-device +Totals: 3 passed, 0 failed (3 items) diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-smoke.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-smoke.txt new file mode 100644 index 0000000..4a0672a --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/final-smoke.txt @@ -0,0 +1,881 @@ +I [2026-10-13T05:05:54.293Z] Starting log, system up 0 second(s), 0 printer(s), listening for connections on '63e88ac90ef5.local:8000' from up to 32768 clients. +I [2026-10-13T05:05:54.297Z] Listening for connections on '*:8000'. +I [2026-10-13T05:05:54.297Z] Listening for connections on '*:8000'. +I [2026-10-13T05:05:54.297Z] Printer 'ptouch' at resource path '/ipp/print/ptouch'. +W [2026-10-13T05:05:54.297Z] [Printer ptouch] Driver does not support identification. +W [2026-10-13T05:05:54.297Z] [Printer ptouch] Driver does not support a self-test page. +W [2026-10-13T05:05:54.297Z] [Printer ptouch] Driver does not provide printer icons, using defaults. +I [2026-10-13T05:05:54.297Z] Created printer for socket://host.docker.internal:57703 (PTOUCH_DEVICE_URI override) +I [2026-10-13T05:05:54.297Z] Listening for connections on '/run/ptouch-app.sock'. +I [2026-10-13T05:05:54.297Z] Starting system. +I [2026-10-13T05:05:54.302Z] [Client 1] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:05:54.302Z] [Client 1] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:05:54.302Z] [Client 1] Continue +I [2026-10-13T05:05:54.302Z] [Client 1] Get-Printer-Attributes successful-ok +I [2026-10-13T05:05:54.302Z] [Client 1] OK application/ipp 10412 +I [2026-10-13T05:05:54.310Z] [Client 1] Closing connection from '172.17.0.1'. +I [2026-10-13T05:05:54.315Z] [Client 2] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:05:54.316Z] [Client 2] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:05:54.316Z] [Client 2] Continue +I [2026-10-13T05:05:54.317Z] [Job 1] Received 153 bytes of document data. +I [2026-10-13T05:05:54.317Z] [Client 2] Print-Job successful-ok +I [2026-10-13T05:05:54.317Z] [Client 2] OK application/ipp 218 +I [2026-10-13T05:05:54.317Z] [Job 1] Starting print job. +I [2026-10-13T05:05:54.317Z] [Job 1] PNG image dimensions are 76x180x1 +I [2026-10-13T05:05:54.317Z] [Job 1] PNG image resolution is 180x180dpi +I [2026-10-13T05:05:54.317Z] [Job 1] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:05:54.318Z] [Client 2] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:05:54.318Z] [Client 2] Continue +I [2026-10-13T05:05:54.318Z] [Client 2] Get-Job-Attributes successful-ok +I [2026-10-13T05:05:54.318Z] [Client 2] OK application/ipp 765 +I [2026-10-13T05:05:54.324Z] [Job 1] Completed, job-impressions-completed=3. +I [2026-10-13T05:05:57.763Z] [Client 3] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:05:57.763Z] [Client 3] HTTP_STATE_POST http://63e88ac90ef5.local:8000/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:05:57.763Z] [Client 3] Continue +I [2026-10-13T05:05:57.763Z] [Client 3] Get-Printer-Attributes successful-ok +I [2026-10-13T05:05:57.764Z] [Client 3] OK application/ipp 14633 +I [2026-10-13T05:05:57.768Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:05:57.768Z] [Client 3] Closing connection from '172.17.0.1'. +I [2026-10-13T05:05:59.319Z] [Client 2] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:05:59.319Z] [Client 2] Continue +I [2026-10-13T05:05:59.319Z] [Client 2] Get-Job-Attributes successful-ok +I [2026-10-13T05:05:59.319Z] [Client 2] OK application/ipp 795 +I [2026-10-13T05:05:59.321Z] [Client 2] Closing connection from '172.17.0.1'. +I [2026-10-13T05:05:59.327Z] [Client 4] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:05:59.328Z] [Client 4] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:05:59.328Z] [Client 4] Continue +I [2026-10-13T05:05:59.328Z] [Job 2] Received 153 bytes of document data. +I [2026-10-13T05:05:59.328Z] [Client 4] Print-Job successful-ok +I [2026-10-13T05:05:59.328Z] [Client 4] OK application/ipp 218 +I [2026-10-13T05:05:59.328Z] [Job 2] Starting print job. +I [2026-10-13T05:05:59.328Z] [Job 2] PNG image dimensions are 76x180x1 +I [2026-10-13T05:05:59.328Z] [Job 2] PNG image resolution is 180x180dpi +I [2026-10-13T05:05:59.328Z] [Job 2] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:05:59.329Z] [Client 4] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:05:59.329Z] [Client 4] Continue +I [2026-10-13T05:05:59.329Z] [Client 4] Get-Job-Attributes successful-ok +I [2026-10-13T05:05:59.329Z] [Client 4] OK application/ipp 784 +I [2026-10-13T05:05:59.336Z] [Job 2] Completed, job-impressions-completed=3. +I [2026-10-13T05:06:04.330Z] [Client 4] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:04.330Z] [Client 4] Continue +I [2026-10-13T05:06:04.330Z] [Client 4] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:04.330Z] [Client 4] OK application/ipp 814 +I [2026-10-13T05:06:04.332Z] [Client 4] Closing connection from '172.17.0.1'. +I [2026-10-13T05:06:04.338Z] [Client 5] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:06:04.338Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:06:04.339Z] [Client 5] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:04.339Z] [Client 5] Continue +I [2026-10-13T05:06:04.339Z] [Job 3] Received 153 bytes of document data. +I [2026-10-13T05:06:04.339Z] [Client 5] Print-Job successful-ok +I [2026-10-13T05:06:04.339Z] [Client 5] OK application/ipp 218 +I [2026-10-13T05:06:04.339Z] [Job 3] Starting print job. +I [2026-10-13T05:06:04.340Z] [Job 3] PNG image dimensions are 76x180x1 +I [2026-10-13T05:06:04.340Z] [Job 3] PNG image resolution is 180x180dpi +I [2026-10-13T05:06:04.340Z] [Job 3] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:06:04.340Z] [Client 5] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:04.340Z] [Client 5] Continue +I [2026-10-13T05:06:04.340Z] [Client 5] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:04.340Z] [Client 5] OK application/ipp 829 +I [2026-10-13T05:06:04.348Z] [Job 3] Completed, job-impressions-completed=3. +I [2026-10-13T05:06:09.342Z] [Client 5] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:09.342Z] [Client 5] Continue +I [2026-10-13T05:06:09.342Z] [Client 5] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:09.342Z] [Client 5] OK application/ipp 859 +I [2026-10-13T05:06:09.343Z] [Client 5] Closing connection from '172.17.0.1'. +I [2026-10-13T05:06:09.349Z] [Client 6] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:06:09.349Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:06:09.350Z] [Client 6] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:09.350Z] [Client 6] Continue +I [2026-10-13T05:06:09.350Z] [Job 4] Received 153 bytes of document data. +I [2026-10-13T05:06:09.350Z] [Client 6] Print-Job successful-ok +I [2026-10-13T05:06:09.350Z] [Client 6] OK application/ipp 218 +I [2026-10-13T05:06:09.350Z] [Job 4] Starting print job. +I [2026-10-13T05:06:09.351Z] [Job 4] PNG image dimensions are 76x180x1 +I [2026-10-13T05:06:09.351Z] [Job 4] PNG image resolution is 180x180dpi +I [2026-10-13T05:06:09.351Z] [Job 4] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:06:09.351Z] [Client 6] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:09.351Z] [Client 6] Continue +I [2026-10-13T05:06:09.351Z] [Client 6] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:09.351Z] [Client 6] OK application/ipp 784 +I [2026-10-13T05:06:09.359Z] [Job 4] Completed, job-impressions-completed=3. +I [2026-10-13T05:06:14.353Z] [Client 6] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:14.353Z] [Client 6] Continue +I [2026-10-13T05:06:14.353Z] [Client 6] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:14.353Z] [Client 6] OK application/ipp 814 +I [2026-10-13T05:06:14.354Z] [Client 6] Closing connection from '172.17.0.1'. +I [2026-10-13T05:06:14.360Z] [Client 7] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:06:14.360Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:06:14.361Z] [Client 7] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:14.361Z] [Client 7] Continue +I [2026-10-13T05:06:14.361Z] [Job 5] Received 153 bytes of document data. +I [2026-10-13T05:06:14.361Z] [Client 7] Print-Job successful-ok +I [2026-10-13T05:06:14.361Z] [Client 7] OK application/ipp 218 +I [2026-10-13T05:06:14.361Z] [Job 5] Starting print job. +I [2026-10-13T05:06:14.362Z] [Job 5] PNG image dimensions are 76x180x1 +I [2026-10-13T05:06:14.362Z] [Job 5] PNG image resolution is 180x180dpi +I [2026-10-13T05:06:14.362Z] [Job 5] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:06:14.362Z] [Client 7] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:14.362Z] [Client 7] Continue +I [2026-10-13T05:06:14.362Z] [Client 7] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:14.362Z] [Client 7] OK application/ipp 784 +I [2026-10-13T05:06:14.370Z] [Job 5] Completed, job-impressions-completed=3. +I [2026-10-13T05:06:19.364Z] [Client 7] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:19.364Z] [Client 7] Continue +I [2026-10-13T05:06:19.364Z] [Client 7] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:19.364Z] [Client 7] OK application/ipp 814 +I [2026-10-13T05:06:19.366Z] [Client 7] Closing connection from '172.17.0.1'. +I [2026-10-13T05:06:19.427Z] [Client 8] Accepted connection from 'localhost'. +I [2026-10-13T05:06:19.427Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:06:19.427Z] [Client 8] HTTP_STATE_POST http://localhost:0/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:19.427Z] [Client 8] Continue +I [2026-10-13T05:06:19.427Z] [Client 8] Get-Printer-Attributes successful-ok +I [2026-10-13T05:06:19.428Z] [Client 8] OK application/ipp 10197 +I [2026-10-13T05:06:19.429Z] [Client 8] HTTP_STATE_POST http://localhost:0/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:19.429Z] [Client 8] Continue +I [2026-10-13T05:06:19.429Z] [Client 8] Set-Printer-Attributes successful-ok (Printer attributes set.) +I [2026-10-13T05:06:19.429Z] [Client 8] OK application/ipp 117 +I [2026-10-13T05:06:19.429Z] [Client 8] Closing connection from 'localhost'. +I [2026-10-13T05:06:19.441Z] [Client 9] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:06:19.441Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:06:19.442Z] [Client 9] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:19.442Z] [Client 9] Continue +I [2026-10-13T05:06:19.442Z] [Client 9] Get-Printer-Attributes successful-ok +I [2026-10-13T05:06:19.442Z] [Client 9] OK application/ipp 174 +I [2026-10-13T05:06:19.442Z] [Client 9] Closing connection from '172.17.0.1'. +I [2026-10-13T05:06:19.448Z] [Client 10] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:06:19.449Z] [Client 10] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:19.449Z] [Client 10] Continue +I [2026-10-13T05:06:19.449Z] [Job 6] Received 153 bytes of document data. +I [2026-10-13T05:06:19.449Z] [Client 10] Print-Job successful-ok +I [2026-10-13T05:06:19.449Z] [Client 10] OK application/ipp 218 +I [2026-10-13T05:06:19.449Z] [Job 6] Starting print job. +I [2026-10-13T05:06:19.449Z] [Job 6] PNG image dimensions are 76x180x1 +I [2026-10-13T05:06:19.449Z] [Job 6] PNG image resolution is 180x180dpi +I [2026-10-13T05:06:19.449Z] [Job 6] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:06:19.450Z] [Client 10] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:19.450Z] [Client 10] Continue +I [2026-10-13T05:06:19.450Z] [Client 10] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:19.450Z] [Client 10] OK application/ipp 765 +I [2026-10-13T05:06:19.458Z] [Job 6] Completed, job-impressions-completed=2. +I [2026-10-13T05:06:24.451Z] [Client 10] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:24.451Z] [Client 10] Continue +I [2026-10-13T05:06:24.451Z] [Client 10] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:24.451Z] [Client 10] OK application/ipp 795 +I [2026-10-13T05:06:24.453Z] [Client 10] Closing connection from '172.17.0.1'. +I [2026-10-13T05:06:24.515Z] [Client 11] Accepted connection from 'localhost'. +I [2026-10-13T05:06:24.515Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:06:24.515Z] [Client 11] HTTP_STATE_POST http://localhost:0/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:24.515Z] [Client 11] Continue +I [2026-10-13T05:06:24.515Z] [Client 11] Get-Printer-Attributes successful-ok +I [2026-10-13T05:06:24.515Z] [Client 11] OK application/ipp 10207 +I [2026-10-13T05:06:24.517Z] [Client 11] HTTP_STATE_POST http://localhost:0/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:24.517Z] [Client 11] Continue +I [2026-10-13T05:06:24.517Z] [Client 11] Set-Printer-Attributes successful-ok (Printer attributes set.) +I [2026-10-13T05:06:24.517Z] [Client 11] OK application/ipp 117 +I [2026-10-13T05:06:24.517Z] [Client 11] Closing connection from 'localhost'. +I [2026-10-13T05:06:24.528Z] [Client 12] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:06:24.528Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:06:24.529Z] [Client 12] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:24.529Z] [Client 12] Continue +I [2026-10-13T05:06:24.529Z] [Client 12] Get-Printer-Attributes successful-ok +I [2026-10-13T05:06:24.529Z] [Client 12] OK application/ipp 127 +I [2026-10-13T05:06:24.530Z] [Client 12] Closing connection from '172.17.0.1'. +I [2026-10-13T05:06:24.535Z] [Client 13] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:06:24.536Z] [Client 13] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:24.536Z] [Client 13] Continue +I [2026-10-13T05:06:24.536Z] [Job 7] Received 153 bytes of document data. +I [2026-10-13T05:06:24.536Z] [Client 13] Print-Job successful-ok +I [2026-10-13T05:06:24.536Z] [Client 13] OK application/ipp 218 +I [2026-10-13T05:06:24.536Z] [Job 7] Starting print job. +I [2026-10-13T05:06:24.537Z] [Job 7] PNG image dimensions are 76x180x1 +I [2026-10-13T05:06:24.537Z] [Job 7] PNG image resolution is 180x180dpi +I [2026-10-13T05:06:24.537Z] [Job 7] portrait image, 180 px along the tape at 180 dpi -> media length 5000 (1/100 mm), header.cupsHeight=354 PWG_ImageBox=[4 0 79 353] +I [2026-10-13T05:06:24.537Z] [Client 13] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:24.537Z] [Client 13] Continue +I [2026-10-13T05:06:24.537Z] [Client 13] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:24.537Z] [Client 13] OK application/ipp 765 +I [2026-10-13T05:06:24.545Z] [Job 7] Completed, job-impressions-completed=2. +I [2026-10-13T05:06:29.538Z] [Client 13] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:29.539Z] [Client 13] Continue +I [2026-10-13T05:06:29.539Z] [Client 13] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:29.539Z] [Client 13] OK application/ipp 795 +I [2026-10-13T05:06:29.540Z] [Client 13] Closing connection from '172.17.0.1'. +I [2026-10-13T05:06:29.546Z] [Client 14] Accepted connection from '172.17.0.1'. +I [2026-10-13T05:06:29.546Z] Saving system state to '/var/lib/ptouch-app.state'. +I [2026-10-13T05:06:29.547Z] [Client 14] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:29.547Z] [Client 14] Continue +I [2026-10-13T05:06:29.547Z] [Job 8] Received 8 bytes of document data. +I [2026-10-13T05:06:29.547Z] [Client 14] Print-Job successful-ok +I [2026-10-13T05:06:29.547Z] [Client 14] OK application/ipp 218 +I [2026-10-13T05:06:29.547Z] [Job 8] Starting print job. +E [2026-10-13T05:06:29.548Z] [Job 8] PNG: Read Error +libpng error: Read Error +I [2026-10-13T05:06:29.548Z] [Job 8] Aborted, job-impressions-completed=0. +I [2026-10-13T05:06:29.548Z] [Client 14] HTTP_STATE_POST http://127.0.0.1:32771/ipp/print/ptouch HTTP/1.1 () +I [2026-10-13T05:06:29.548Z] [Client 14] Continue +I [2026-10-13T05:06:29.548Z] [Client 14] Get-Job-Attributes successful-ok +I [2026-10-13T05:06:29.548Z] [Client 14] OK application/ipp 788 +I [2026-10-13T05:06:29.550Z] [Client 14] Closing connection from '172.17.0.1'. +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Get-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + requested-attributes (keyword) = all + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 10412 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + charset-configured (charset) = utf-8 + charset-supported (1setOf charset) = us-ascii,utf-8 + client-info-supported (1setOf keyword) = client-name,client-patches,client-string-version,client-version + compression-supported (1setOf keyword) = deflate,gzip,none + copies-default (integer) = 1 + device-uuid (uri) = urn:uuid:967729b2-957c-3818-6fab-e58da5179a9c + document-format-default (mimeMediaType) = application/octet-stream + generated-natural-language-supported (naturalLanguage) = en + ipp-versions-supported (1setOf keyword) = 1.1,2.0 + job-hold-until-default (keyword) = no-hold + job-hold-until-supported (1setOf keyword) = day-time,evening,indefinite,night,no-hold,second-shift,third-shift,weekend + job-hold-until-time-supported (boolean) = true + job-ids-supported (boolean) = true + job-k-octets-supported (rangeOfInteger) = 0-101803280 + job-priority-default (integer) = 50 + job-priority-supported (integer) = 1 + job-sheets-default (nameWithoutLanguage) = none + job-sheets-supported (nameWithoutLanguage) = none + multiple-document-handling-supported (1setOf keyword) = separate-documents-uncollated-copies,separate-documents-collated-copies + multiple-document-jobs-supported (boolean) = false + multiple-operation-time-out (integer) = 60 + multiple-operation-time-out-action (keyword) = abort-job + natural-language-configured (naturalLanguage) = en + notify-events-default (keyword) = job-completed + notify-events-supported (1setOf keyword) = document-completed,document-config-changed,document-created,document-fetchable,document-state-changed,document-stopped,job-completed,job-config-changed,job-created,job-fetchable,job-progress,job-state-changed,job-stopped,printer-config-changed,printer-finishings-changed,printer-media-changed,printer-queue-order-changed,printer-restarted,printer-shutdown,printer-state-changed,printer-stopped,resource-canceled,resource-config-changed,resource-created,resource-installed,resource-changed,printer-created,printer-deleted,system-config-changed,system-state-changed,system-stopped + notify-lease-duration-default (integer) = 3600 + notify-lease-duration-supported (rangeOfInteger) = 0-86400 + notify-max-events-supported (integer) = 100 + notify-pull-method-supported (keyword) = ippget + operations-supported (1setOf enum) = Print-Job,Validate-Job,Create-Job,Send-Document,Cancel-Job,Get-Job-Attributes,Get-Jobs,Get-Printer-Attributes,Pause-Printer,Resume-Printer,Set-Printer-Attributes,Get-Printer-Supported-Values,Create-Printer-Subscriptions,Create-Job-Subscriptions,Get-Subscription-Attributes,Get-Subscriptions,Renew-Subscription,Cancel-Subscription,Get-Notifications,Enable-Printer,Disable-Printer,Pause-Printer-After-Current-Job,Cancel-Current-Job,Cancel-Jobs,Cancel-My-Jobs,Close-Job,Identify-Printer,Hold-Job,Release-Job,Hold-New-Jobs,Release-Held-New-Jobs + orientation-requested-supported (1setOf enum) = portrait,landscape,reverse-landscape,reverse-portrait,none + pdl-override-supported (keyword) = attempted + print-content-optimize-supported (1setOf keyword) = auto,graphic,photo,text-and-graphic,text + print-processing-attributes-supported (1setOf keyword) = print-color-mode,printer-resolution + print-quality-supported (1setOf enum) = draft,normal,high + print-scaling-supported (1setOf keyword) = auto,auto-fit,fill,fit,none + printer-get-attributes-supported (keyword) = document-format + printer-info (textWithoutLanguage) = ptouch + printer-name (nameWithoutLanguage) = ptouch + printer-uuid (uri) = urn:uuid:967729b2-957c-3818-6fab-e58da5179a9c + requesting-user-uri-supported (boolean) = true + smi55357-device-uri (uri) = socket://host.docker.internal:57703 + smi55357-driver (keyword) = brother_ptouch + uri-security-supported (1setOf keyword) = tls,none + which-jobs-supported (1setOf keyword) = completed,not-completed,all + printer-id (integer) = 1 + color-supported (boolean) = false + document-format-preferred (mimeMediaType) = image/urf + document-format-supported (1setOf mimeMediaType) = application/octet-stream,image/pwg-raster,image/urf,image/png,image/jpeg + ipp-features-supported (keyword) = ipp-everywhere + job-creation-attributes-supported (1setOf keyword) = copies,document-format,document-name,ipp-attribute-fidelity,job-hold-until,job-hold-until-time,job-name,job-priority,job-retain-until,job-retain-until-interval,job-retain-until-time,media,media-col,multiple-document-handling,orientation-requested,print-color-mode,print-content-optimize,print-quality,printer-resolution,finishings,finishings-col + jpeg-features-supported (1setOf keyword) = arithmetic,cmyk,progressive + jpeg-k-octets-supported (rangeOfInteger) = 0-101803280 + jpeg-x-dimension-supported (rangeOfInteger) = 0-16384 + jpeg-y-dimension-supported (rangeOfInteger) = 1-16384 + landscape-orientation-requested-preferred (enum) = 4 + media-bottom-margin-supported (integer) = 0 + media-col-supported (1setOf keyword) = media-bottom-margin,media-left-margin,media-right-margin,media-size,media-size-name,media-top-margin,media-source,media-type + media-source-supported (1setOf keyword) = main-roll,auto + media-supported (1setOf keyword) = om_6x25mm_6x25mm,om_6x50mm_6x50mm,om_6x100mm_6x100mm,om_9x25mm_9x25mm,om_9x50mm_9x50mm,om_9x100mm_9x100mm,om_12x25mm_12x25mm,om_12x50mm_12x50mm,om_12x100mm_12x100mm,om_18x25mm_18x25mm,om_18x50mm_18x50mm,om_18x100mm_18x100mm,om_24x25mm_24x25mm,om_24x50mm_24x50mm,om_24x100mm_24x100mm,roll_min_6x5mm,roll_max_24x300mm + media-top-margin-supported (integer) = 0 + media-type-supported (keyword) = labels + mopria-certified (textWithoutLanguage) = 1.3 + output-bin-supported (keyword) = face-down + pages-per-minute (integer) = 20 + print-color-mode-supported (1setOf keyword) = bi-level,monochrome + printer-device-id (textWithoutLanguage) = MFG:Brother;MDL:PT-2730;CMD:PT-CBP; + printer-kind (keyword) = label + printer-make-and-model (textWithoutLanguage) = Brother PT-2730 + printer-output-tray (octetString) = type=unRemovableBin;maxcapacity=-2;remaining=-2;status=0;name=face-down;stackingorder=firstToLast;pagedelivery=faceDown; + printer-resolution-supported (resolution) = 180dpi + printer-settable-attributes (1setOf keyword) = copies-default,media-col-default,media-col-ready,media-default,media-ready,multiple-document-handling-default,orientation-requested-default,print-color-mode-default,print-content-optimize-default,print-quality-default,printer-contact-col,printer-geo-location,printer-location,printer-organization,printer-organizational-unit,printer-resolution-default,finishings-col-default,finishings-default + pwg-raster-document-resolution-supported (resolution) = 180dpi + pwg-raster-document-type-supported (1setOf keyword) = black_1,black_8 + sides-supported (keyword) = one-sided + urf-supported (1setOf keyword) = V1.5,W8,DEVW8,PQ3-4-5,FN3-11,IS0-9,MT5,OB10,RS180 + finishings-supported (1setOf enum) = none,trim,trim-after-pages,trim-after-job + finishings-ready (1setOf enum) = none,trim,trim-after-pages,trim-after-job + finishings-col-supported (keyword) = finishing-template + finishing-template-supported (1setOf keyword) = trim,trim-after-pages,trim-after-job,ptouch-chain + finishings-col-database (1setOf collection) = {finishing-template=trim},{finishing-template=trim-after-pages},{finishing-template=trim-after-job},{finishing-template=ptouch-chain} + finishings-col-ready (1setOf collection) = {finishing-template=trim},{finishing-template=trim-after-pages},{finishing-template=trim-after-job},{finishing-template=ptouch-chain} + media-size-supported (1setOf collection) = {x-dimension=599-601 y-dimension=500-30000},{x-dimension=899-901 y-dimension=500-30000},{x-dimension=1199-1201 y-dimension=500-30000},{x-dimension=1799-1801 y-dimension=500-30000},{x-dimension=2399-2401 y-dimension=500-30000} + media-left-margin-supported (1setOf integer) = 53,63,74,83,297 + media-right-margin-supported (1setOf integer) = 57,71,74,85,297 + finishings-default (enum) = trim + finishings-col-default (collection) = {finishing-template=trim} + printer-is-accepting-jobs (boolean) = true + printer-state (enum) = idle + printer-state-message (textWithoutLanguage) = Idle. + printer-state-reasons (keyword) = none + copies-supported (rangeOfInteger) = 1-999 + identify-actions-default (keyword) = none + job-spooling-supported (keyword) = spool + media-col-default (collection) = {media-bottom-margin=0 media-left-margin=63 media-right-margin=71 media-size={x-dimension=1200 y-dimension=10000} media-size-name=om_12x100mm_12x100mm media-source=main-roll media-top-margin=0 media-type=labels} + media-col-ready (collection) = {media-bottom-margin=0 media-left-margin=63 media-right-margin=71 media-size={x-dimension=1200 y-dimension=10000} media-size-name=om_12x100mm_12x100mm media-source=main-roll media-top-margin=0 media-type=labels} + media-default (keyword) = om_12x100mm_12x100mm + media-ready (keyword) = om_12x100mm_12x100mm + multiple-document-handling-default (keyword) = separate-documents-collated-copies + orientation-requested-default (enum) = none + output-bin-default (keyword) = face-down + print-color-mode-default (keyword) = bi-level + print-content-optimize-default (keyword) = auto + print-quality-default (enum) = normal + print-scaling-default (keyword) = none + printer-config-change-date-time (dateTime) = 2026-10-13T05:05:54Z + printer-config-change-time (integer) = 1 + printer-contact-col (collection) = {contact-name= contact-uri=data:, contact-vcard=BEGIN:VCARD +VERSION:4.0 +FN: +TEL;VALUE=URI;TYPE=work: +EMAIL;TYPE=work: +END:VCARD +} + printer-current-time (dateTime) = 2026-10-13T05:05:54Z + printer-dns-sd-name (nameWithoutLanguage) = ptouch + printer-firmware-name (1setOf nameWithoutLanguage) = ptouch-app,PAPPL,libcups,libjpeg,libpng + printer-firmware-patches (1setOf textWithoutLanguage) = ,,,, + printer-firmware-string-version (1setOf textWithoutLanguage) = 1.0,1.4.11,2.4.2,8.0,1.6.39 + printer-firmware-version (1setOf octetString) = \001\000\000\000\000\000\000\000,\001\000\004\000\ \000\000\000,\002\000\004\000\002\000\000\000,\010\000\000\000\000\000\000\000,\001\000\006\000'\000\000\000 + printer-geo-location (unknown) = unknown + printer-icons (1setOf uri) = http://127.0.0.1:32771/ptouch/icon-sm.png,http://127.0.0.1:32771/ptouch/icon-md.png,http://127.0.0.1:32771/ptouch/icon-lg.png + printer-impressions-completed (integer) = 0 + printer-input-tray (1setOf octetString) = type=sheetFeedAutoRemovableTray;mediafeed=10000;mediaxfeed=1200;maxcapacity=-2;level=-2;status=0;name=main-roll;,type=other;mediafeed=0;mediaxfeed=0;maxcapacity=-2;level=-2;status=0;name=auto; + printer-location (textWithoutLanguage) = + printer-more-info (uri) = http://127.0.0.1:32771/ptouch/ + printer-organization (textWithoutLanguage) = + printer-organizational-unit (textWithoutLanguage) = + printer-resolution-default (resolution) = 180dpi + printer-speed-default (integer) = 0 + printer-state-change-date-time (dateTime) = 2026-10-13T05:05:54Z + printer-state-change-time (integer) = 0 + printer-supply-info-uri (uri) = http://127.0.0.1:32771/ptouch/supplies + printer-up-time (integer) = 0 + printer-uri-supported (1setOf uri) = ipps://127.0.0.1:32771/ipp/print/ptouch,ipp://127.0.0.1:32771/ipp/print/ptouch + printer-xri-supported (1setOf collection) = {xri-authentication=none xri-security=none xri-uri=ipp://127.0.0.1:32771/ipp/print/ptouch},{xri-authentication=none xri-security=tls xri-uri=ipps://127.0.0.1:32771/ipp/print/ptouch} + queued-job-count (integer) = 0 + sides-default (keyword) = one-sided + uri-authentication-supported (1setOf keyword) = none,none + +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 1 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/1 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 1 + /tmp/ptouch-smoke-zrfzh4ta/request [0001] + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 795 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 1 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/1 + job-uuid (uri) = urn:uuid:7c3a94a6-6050-3147-6578-f51eea77a161 + job-printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:05:54Z + date-time-at-completed (dateTime) = 2026-10-13T05:05:54Z + date-time-at-processing (dateTime) = 2026-10-13T05:05:54Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 3 + job-printer-up-time (integer) = 5 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 0 + time-at-completed (integer) = 0 + time-at-processing (integer) = 0 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +default {"pages": [{"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "eject", "autocut": true, "chain_off_at_print": true}], "terminal_commands_after_pages": [], "init": 1, "eject": 1} +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings (enum) = none + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 2 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/2 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 2 + /tmp/ptouch-smoke-zrfzh4ta/request [0001] + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 814 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings (enum) = none + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 2 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/2 + job-uuid (uri) = urn:uuid:7613655c-e38c-3b50-4e73-5c7cb6e8dbfa + job-printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:05:59Z + date-time-at-completed (dateTime) = 2026-10-13T05:05:59Z + date-time-at-processing (dateTime) = 2026-10-13T05:05:59Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 3 + job-printer-up-time (integer) = 10 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 5 + time-at-completed (integer) = 5 + time-at-processing (integer) = 5 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +none {"pages": [{"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": false, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": false, "chain_off_at_print": false}], "terminal_commands_after_pages": [], "init": 1, "eject": 0} +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings-col (collection) = {finishing-template=ptouch-chain} + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 3 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/3 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 3 + /tmp/ptouch-smoke-zrfzh4ta/request [0001] + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 859 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings-col (collection) = {finishing-template=ptouch-chain} + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 3 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/3 + job-uuid (uri) = urn:uuid:c58fd4b0-5751-3e4a-4c1f-d607cc50e48f + job-printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:06:04Z + date-time-at-completed (dateTime) = 2026-10-13T05:06:04Z + date-time-at-processing (dateTime) = 2026-10-13T05:06:04Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 3 + job-printer-up-time (integer) = 15 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 10 + time-at-completed (integer) = 10 + time-at-processing (integer) = 10 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +chain {"pages": [{"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}], "terminal_commands_after_pages": [], "init": 0, "eject": 0} +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings (enum) = trim-after-pages + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 4 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/4 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 4 + /tmp/ptouch-smoke-zrfzh4ta/request [0001] + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 814 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings (enum) = trim-after-pages + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 4 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/4 + job-uuid (uri) = urn:uuid:358db792-fba1-313b-5e4e-7ae7bf44a420 + job-printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:06:09Z + date-time-at-completed (dateTime) = 2026-10-13T05:06:09Z + date-time-at-processing (dateTime) = 2026-10-13T05:06:09Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 3 + job-printer-up-time (integer) = 20 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 15 + time-at-completed (integer) = 15 + time-at-processing (integer) = 15 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +each {"pages": [{"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "eject", "autocut": true, "chain_off_at_print": true}], "terminal_commands_after_pages": [], "init": 0, "eject": 1} +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings (enum) = trim-after-job + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 5 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/5 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 5 + /tmp/ptouch-smoke-zrfzh4ta/request [0001] + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 814 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 3 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + finishings (enum) = trim-after-job + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 5 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/5 + job-uuid (uri) = urn:uuid:3f951106-859c-3ad9-7f44-45fbe4b59bb3 + job-printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:06:14Z + date-time-at-completed (dateTime) = 2026-10-13T05:06:14Z + date-time-at-processing (dateTime) = 2026-10-13T05:06:14Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 3 + job-printer-up-time (integer) = 25 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 20 + time-at-completed (integer) = 20 + time-at-processing (integer) = 20 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +end {"pages": [{"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": false, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "eject", "autocut": false, "chain_off_at_print": true}], "terminal_commands_after_pages": [], "init": 1, "eject": 1} + +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Get-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + requested-attributes (1setOf keyword) = finishings-default,finishings-col-default + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 174 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + finishings-default (enum) = trim-after-job + finishings-col-default (collection) = {finishing-template=trim-after-job} + +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 2 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 6 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/6 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 6 + /tmp/ptouch-smoke-zrfzh4ta/request [0001] + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 795 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 2 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 6 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/6 + job-uuid (uri) = urn:uuid:e5751cb7-9e44-3b2b-534c-a2775932077f + job-printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:06:19Z + date-time-at-completed (dateTime) = 2026-10-13T05:06:19Z + date-time-at-processing (dateTime) = 2026-10-13T05:06:19Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 2 + job-printer-up-time (integer) = 30 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 25 + time-at-completed (integer) = 25 + time-at-processing (integer) = 25 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +default 63 {"pages": [{"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "eject", "autocut": false, "chain_off_at_print": true}], "terminal_commands_after_pages": [], "init": 1, "eject": 1} + +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Get-Printer-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + requested-attributes (1setOf keyword) = finishings-default,finishings-col-default + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 127 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + finishings-default (enum) = none + finishings-col-default (no-value) = no-value + +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 2 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 7 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/7 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 7 + /tmp/ptouch-smoke-zrfzh4ta/request [0001] + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 795 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 2 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 7 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/7 + job-uuid (uri) = urn:uuid:3f8e7022-46a8-3bb3-64ce-c28417491696 + job-printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:06:24Z + date-time-at-completed (dateTime) = 2026-10-13T05:06:24Z + date-time-at-processing (dateTime) = 2026-10-13T05:06:24Z + job-impressions (integer) = 1 + job-impressions-completed (integer) = 2 + job-printer-up-time (integer) = 35 + job-state (enum) = completed + job-state-message (textWithoutLanguage) = Job completed. + job-state-reasons (keyword) = job-completed-successfully + time-at-creation (integer) = 30 + time-at-completed (integer) = 30 + time-at-processing (integer) = 30 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +default 3 {"pages": [{"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": true, "chain_off_at_print": false}, {"rows": 354, "length_mm": 49.95, "finish": "ff", "autocut": false, "chain_off_at_print": false}], "terminal_commands_after_pages": [], "init": 1, "eject": 0} +"/tmp/ptouch-smoke-zrfzh4ta/request.test": + Print-Job: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + document-format (mimeMediaType) = image/png + copies (integer) = 1 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 218 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + job-id (integer) = 8 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/8 + job-state (enum) = pending + job-state-message (textWithoutLanguage) = Job pending. + job-state-reasons (keyword) = none + Get-Job-Attributes: + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + requesting-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 8 + /tmp/ptouch-smoke-zrfzh4ta/request [PASS] + RECEIVED: 788 bytes in response + status-code = successful-ok (successful-ok) + attributes-charset (charset) = utf-8 + attributes-natural-language (naturalLanguage) = en + copies (integer) = 1 + media (keyword) = om_12x50mm_12x50mm + print-scaling (keyword) = none + document-format-supplied (mimeMediaType) = image/png + job-name (nameWithoutLanguage) = Untitled + job-originating-user-name (nameWithoutLanguage) = smoke + job-id (integer) = 8 + job-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch/8 + job-uuid (uri) = urn:uuid:a1bcb206-c7c2-320b-407b-4447e44b5e95 + job-printer-uri (uri) = ipp://127.0.0.1:32771/ipp/print/ptouch + date-time-at-creation (dateTime) = 2026-10-13T05:06:29Z + date-time-at-completed (dateTime) = 2026-10-13T05:06:29Z + date-time-at-processing (dateTime) = 2026-10-13T05:06:29Z + job-impressions (integer) = 0 + job-impressions-completed (integer) = 0 + job-printer-up-time (integer) = 35 + job-state (enum) = aborted + job-state-message (textWithoutLanguage) = Job aborted. + job-state-reasons (keyword) = document-format-error + time-at-creation (integer) = 35 + time-at-completed (integer) = 35 + time-at-processing (integer) = 35 + +Summary: 2 tests, 2 passed, 0 failed, 0 skipped +Score: 100% + +Image smoke passed: four presets, synchronized defaults, geometry, copies, and input failure. diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-chain-2.prn b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-chain-2.prn new file mode 100644 index 0000000..e84953f Binary files /dev/null and b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-chain-2.prn differ diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-each-2.prn b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-each-2.prn new file mode 100644 index 0000000..2ba5d4f Binary files /dev/null and b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-each-2.prn differ diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-end-2.prn b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-end-2.prn new file mode 100644 index 0000000..9fdaffc Binary files /dev/null and b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-end-2.prn differ diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-none-2.prn b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-none-2.prn new file mode 100644 index 0000000..2f69eb8 Binary files /dev/null and b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/gate-24mm-none-2.prn differ diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-after.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-after.txt new file mode 100644 index 0000000..3dd9cad --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-after.txt @@ -0,0 +1,81 @@ +E [2026-10-13T04:56:55.804Z] Unable to initialize DNS-SD: Daemon not running +E [2026-10-13T04:56:55.804Z] Unable to initialize DNS-SD: Daemon not running +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=180 pages=1 ends=1/0 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=540 pages=2 ends=1/0 +E [2026-10-13T04:56:55.808Z] [Job 3] Incomplete raster page at row 179. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=179 pages=0 ends=0/1 +E [2026-10-13T04:56:55.809Z] [Job 4] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +E [2026-10-13T04:56:55.809Z] [Job 5] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=180 pages=1 ends=1/0 +E [2026-10-13T04:56:55.810Z] [Job 7] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=180 pages=1 ends=1/0 +E [2026-10-13T04:56:55.811Z] [Job 9] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=3 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=180 pages=1 ends=1/0 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=540 pages=2 ends=1/0 +E [2026-10-13T04:56:55.813Z] [Job 13] Incomplete raster page at row 179. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=179 pages=0 ends=0/1 +E [2026-10-13T04:56:55.814Z] [Job 14] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +E [2026-10-13T04:56:55.814Z] [Job 15] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=180 pages=1 ends=1/0 +E [2026-10-13T04:56:55.815Z] [Job 17] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=180 pages=1 ends=1/0 +E [2026-10-13T04:56:55.816Z] [Job 19] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=3 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=180 pages=1 ends=1/0 +E [2026-10-13T04:56:55.817Z] [Job 22] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=180 pages=1 ends=1/0 +E [2026-10-13T04:56:55.818Z] [Job 24] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +E [2026-10-13T04:56:55.818Z] [Job 25] Unsupported raster data seen. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +E [2026-10-13T04:56:55.819Z] [Job 26] Bad raster data seen. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +E [2026-10-13T04:56:55.824Z] [Job 27] Unsupported raster resolution. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/1 +E [2026-10-13T04:56:55.827Z] [Job 28] Unable to write raster line 3. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=3 pages=0 ends=0/1 +E [2026-10-13T04:56:55.827Z] [Job 29] Unable to end raster job. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=708 pages=1 ends=1/0 +E [2026-10-13T04:56:55.828Z] [Job 30] Unable to finish raster stream - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.829Z] [Job 31] Unable to finish raster stream - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.829Z] [Job 32] Unable to finish raster stream - Invalid raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.830Z] [Job 33] Unable to read raster stream from client - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/0 +E [2026-10-13T04:56:55.833Z] [Job 34] Incomplete raster page at row 359. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=539 pages=1 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=540 pages=2 ends=1/0 +E [2026-10-13T04:56:55.834Z] [Job 36] Unable to finish raster stream - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.834Z] [Job 37] Unable to finish raster stream - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.835Z] [Job 38] Unable to finish raster stream - Invalid raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.836Z] [Job 39] Unable to read raster stream from client - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/0 +E [2026-10-13T04:56:55.836Z] [Job 40] Incomplete raster page at row 359. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=539 pages=1 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=540 pages=2 ends=1/0 +E [2026-10-13T04:56:55.838Z] [Job 42] Unable to finish raster stream - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.838Z] [Job 43] Unable to finish raster stream - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.839Z] [Job 44] Unable to finish raster stream - Invalid raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=180 pages=1 ends=0/1 +E [2026-10-13T04:56:55.839Z] [Job 45] Unable to read raster stream from client - Truncated raster page header.\n +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=0 pages=0 ends=0/0 +E [2026-10-13T04:56:55.840Z] [Job 46] Incomplete raster page at row 359. +/tmp/pappl-raster-qQFtQt/page.ras: state=8 rows=539 pages=1 ends=0/1 +/tmp/pappl-raster-qQFtQt/page.ras: state=9 rows=540 pages=2 ends=1/0 diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-bad-context.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-bad-context.txt new file mode 100644 index 0000000..90c9f9a --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-bad-context.txt @@ -0,0 +1,4 @@ +patching file pappl/job-process.c +Hunk #2 FAILED at 599. +1 out of 9 hunks FAILED -- saving rejects to file pappl/job-process.c.rej +patching file pappl/job-filter.c diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-before.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-before.txt new file mode 100644 index 0000000..459c9a1 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-before.txt @@ -0,0 +1,15 @@ +E [2026-10-13T04:55:28.933Z] Unable to initialize DNS-SD: Daemon not running +E [2026-10-13T04:55:28.933Z] Unable to initialize DNS-SD: Daemon not running +/tmp/pappl-raster-WLjBZa/page.ras: state=9 rows=180 pages=1 ends=1/0 +/tmp/pappl-raster-WLjBZa/page.ras: state=9 rows=540 pages=2 ends=1/0 +E [2026-10-13T04:55:28.935Z] [Job 3] Incomplete raster page at row 179. +/tmp/pappl-raster-WLjBZa/page.ras: state=8 rows=179 pages=0 ends=0/1 +E [2026-10-13T04:55:28.935Z] [Job 4] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-WLjBZa/page.ras: state=8 rows=0 pages=0 ends=0/1 +E [2026-10-13T04:55:28.936Z] [Job 5] Raster page contradicts explicit job media dimensions. +/tmp/pappl-raster-WLjBZa/page.ras: state=8 rows=0 pages=0 ends=0/1 +/tmp/pappl-raster-WLjBZa/page.ras: state=9 rows=180 pages=1 ends=1/0 +/tmp/pappl-raster-WLjBZa/page.ras: state=9 rows=180 pages=1 ends=1/0 +test_before: /src/tests/test_pappl_raster.c:171: execute: Assertion `papplJobGetState(job) == (success ? IPP_JSTATE_COMPLETED : IPP_JSTATE_ABORTED)' failed. +timeout: the monitored command dumped core +Aborted diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-clean.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-clean.txt new file mode 100644 index 0000000..e3a1031 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-clean.txt @@ -0,0 +1,2 @@ +patching file pappl/job-process.c +patching file pappl/job-filter.c diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-duplicate.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-duplicate.txt new file mode 100644 index 0000000..0887b7c --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-duplicate.txt @@ -0,0 +1,6 @@ +patching file pappl/job-process.c +Reversed (or previously applied) patch detected! Skipping patch. +9 out of 9 hunks ignored -- saving rejects to file pappl/job-process.c.rej +patching file pappl/job-filter.c +Reversed (or previously applied) patch detected! Skipping patch. +3 out of 3 hunks ignored -- saving rejects to file pappl/job-filter.c.rej diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-finishings-followup.txt b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-finishings-followup.txt new file mode 100644 index 0000000..6c8819f --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/named-raster-finishings-followup.txt @@ -0,0 +1,6 @@ +patching file pappl/printer-driver.c +patching file pappl/printer-ipp.c +patching file pappl/job-process.c +patching file pappl/finishings-private.h +patching file pappl/system-loadsave.c +patching file pappl/printer-webif.c diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/ptouch-integrated-12mm.tar.gz b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/ptouch-integrated-12mm.tar.gz new file mode 100644 index 0000000..4a85373 Binary files /dev/null and b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/ptouch-integrated-12mm.tar.gz differ diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/usb-capture.c b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/usb-capture.c new file mode 100644 index 0000000..ce19a65 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/evidence/usb-capture.c @@ -0,0 +1,32 @@ +/* Test-only transparent recording; preserves libusb results and byte counts. */ +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +static pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER; +int libusb_bulk_transfer(libusb_device_handle *h, unsigned char endpoint, + unsigned char *bytes, int length, int *actual, unsigned int timeout) +{ + int (*real)(libusb_device_handle *,unsigned char,unsigned char *,int,int *,unsigned int); + real=dlsym(RTLD_NEXT,"libusb_bulk_transfer"); + if (!real) abort(); + int rc=real(h,endpoint,bytes,length,actual,timeout); + int count=actual?*actual:0; + struct timespec now;clock_gettime(CLOCK_MONOTONIC,&now); + pthread_mutex_lock(&lock); + FILE *log=fopen("/evidence/usb-io.jsonl","a"); + if(log){ + fprintf(log,"{\"t\":%ld.%09ld,\"ep\":%u,\"rc\":%d,\"requested\":%d,\"actual\":%d,\"hex\":\"",now.tv_sec,now.tv_nsec,endpoint,rc,length,count); + for(int i=0;i0){ + FILE *out=fopen("/evidence/usb-out.bin","ab"); + if(out){fwrite(bytes,1,count,out);fclose(out);} + } + pthread_mutex_unlock(&lock); + return rc; +} diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/proposal.md b/openspec/changes/archive/2026-10-13-simplify-print-server/proposal.md new file mode 100644 index 0000000..9ca2382 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/proposal.md @@ -0,0 +1,41 @@ +# Proposal + +## Why + +The server should accept ordinary CUPS/IPP jobs with predictable media and finishing behavior, but the audit reproduced multi-page raster failure, silent PNG cropping, ignored explicit media length, and rejection of advertised 18 mm tape. Simplify the existing PAPPL adapter around actual pages and truthful capabilities rather than extending its special client contract. + +## What Changes + +- **BREAKING:** Support four finishing presets: cut between labels and continue, cut between labels and finish, no label cuts and continue, and only finish after the batch. Keep cut-between-and-finish as the initial default, using `trim` as its compatibility alias. +- **BREAKING:** Precut is mandatory once at the beginning of every nonempty batch, including the no-label-cuts and final-only presets. On a healthy open run it separates the previous batch's held end instead of generating a discarded leader; verify this on hardware. Remove the optional precut environment setting. +- One IPP job is one batch, covering PNG copies or actual raster labels. Preserve efficient chaining within and across unfinished batches, including idle gaps; introduce no queue inference, idle cutting, or per-label run finalization. +- Replace the copy-sized operation array with constant-size job state. Defer one label's print/feed command until the next label begins or the batch ends, handling unknown raster label counts without buffering another raster document. Hardware validation gates the late finalization sequence and final-only cutting. +- Expose the presets through standard IPP finishings: registered `none` (3), `trim-after-pages` (60), and `trim-after-job` (63), plus `trim` (11) as an alias. Advertise `ptouch-chain` as an implementation-defined finishing template for chain-only through standard `finishings-col`; do not invent an independent vendor job attribute or redefine 60 to exclude the final label. +- **BREAKING:** Honor explicitly requested IPP media geometry for PNG. Retain image-derived PNG length only when the client omitted a concrete media length; JPEG and standard raster continue using their standard page geometry. +- Reject oversized 1:1 PNG content before raster output instead of silently clipping it, and make all advertised tape widths and margins usable, including 18 mm. Retain JPEG's standard PAPPL placement path without adding another custom decoder. +- Remove the unsupported private raster MIME advertisement and the implementation's 64-copy planner limit; make advertised formats and format-specific copy ranges executable. +- **BREAKING:** Limit operational queues to the verified PT-2730 path, reject ambiguous USB matches, and fail invalid model configuration instead of silently selecting a fallback. +- Keep one application queue and give its state a clear owner; remove the global eight-slot registry while retaining required synchronization and unfinished-run reset protection. +- Centralize safe transport access, report unavailable/error states truthfully, and retain offline queuing and hotplug recovery. Distinguish a healthy unfinished run from uncertain partial output; block continuation from uncertain state until explicit recovery rather than blindly resetting the printer. +- Use normal-level stderr logging without a log-tail process; remove the ineffective width-policy configuration knob from the supported interface. +- Run the existing tests plus the audit regressions in CI before image publication, then require separate PT-2730 physical validation of changed output behavior. + +## Capabilities + +### New Capabilities + +- `label-rendering`: Standard job/page geometry, image sizing, printable-area validation, tape margins, and truthful document/copy capabilities. +- `printer-device`: Supported USB device selection, safe transport access, status reporting, and offline recovery for the persistent queue. + +### Modified Capabilities + +- `batch-printing`: Four finishing presets, mandatory precut, job-defined batch boundaries, continuation without inter-batch leader waste, and failure handling; supersede the former no-precut rule and inaccurate document-format/copy-limit preservation. + +## Impact + +- Implementation: `src/driver.c`, `src/cutter.*`, `src/filter_png.*`, `src/driver.h`, `src/main.c`, `src/device_usb.*`; table entries may remain as recognition metadata but must not grant print support. +- Validation/operations: `tests/`, `CMakeLists.txt`, `Dockerfile.deploy`, `.github/workflows/deploy.yml`, `docker-entrypoint.sh`, README, and affected ADRs. +- Bounded patches to pinned PAPPL 1.4.11 must preserve raster geometry, reject truncated input and failed callbacks, and support the driver's advertised finishing presets in validation and defaults. A bounded CUPS raster-decoder correction must distinguish partial next headers from clean EOF, propagating failure through PAPPL before normal end-job. Pin compatible CUPS source while retaining packaged security corrections. Ship identical dependency patches and runtime libraries in development and deploy builds; add no raster parser, page spooler, or separate maintained fork. +- Keep PAPPL, libusb, libpng, the hardware-free protocol core, bounded USB I/O, and the working USB bind-mount deployment approach. No new network protocol, REST endpoint, model family, daemon, or persistent batch database. +- Existing clients that omitted finishings will now receive cutting; `none` still leaves the batch unfinished but now includes mandatory precut. Consecutive no-label-cuts batches become separate strips at the next batch's precut. PNG media dimensions are honored. The chain-only preset has a printer-defined meaning carried by standard IPP, and generic clients may need collection/template support to select it. +- This proposal authorizes planning only. Implementation remains local until specifications are synced and this change is archived; publication and production rollout require a separate user instruction. diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/specs/batch-printing/spec.md b/openspec/changes/archive/2026-10-13-simplify-print-server/specs/batch-printing/spec.md new file mode 100644 index 0000000..7aa0f87 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/specs/batch-printing/spec.md @@ -0,0 +1,161 @@ +# Spec Delta + +## MODIFIED Requirements + +### Requirement: Job finishing controls cutting + +The server SHALL support four presets: cuts between labels with the batch left open; cuts between labels plus a final batch cut; no label cuts with the batch left open; and only a final batch cut. Mandatory precut is separate batch preparation. `none` selects no label cuts and continuation; `trim` aliases cuts between labels plus the final cut. Queue contents, idle time, and legacy environment settings SHALL NOT choose or override finishing. + +#### Scenario: Explicit no-cut job +- **WHEN** a valid job requests finishings none +- **THEN** mandatory precut prepares the first label, and the batch prints without cuts between its labels or a final cut, even if the queue becomes empty + +#### Scenario: Explicit trim job +- **WHEN** a valid job requests finishings trim +- **THEN** its labels are cut apart using one chained run, and only the batch's last label receives final feed/cut, even if another job is queued + +#### Scenario: Finishings collection +- **WHEN** a job selects a supported finishing template through finishings-col +- **THEN** it uses that template's advertised preset, and registered templates behave like their corresponding finishings enums + +#### Scenario: Chain cuts without final cut +- **WHEN** a three-label batch requests the chain-only preset +- **THEN** its first two labels are separated and its last label remains attached without terminal feed/cut + +#### Scenario: Final cut only +- **WHEN** a three-label batch requests trim-after-job +- **THEN** mandatory precut occurs once, the three labels remain joined, and one final cut releases the strip + +### Requirement: Advertised defaults are honored + +The server SHALL initialize finishings-default and finishings-col-default consistently to trim. An omitted job selection SHALL use one canonical default, updated through standard management of either representation. Defaults SHALL support the three presets with enum equivalents, with collection no-value for enum none. Chain-only and contradictory default updates SHALL be rejected without changing defaults. Explicit job selections SHALL override the default without hidden environment or queue-based policy. + +#### Scenario: No finishing option +- **WHEN** the client omits finishings and the advertised default is trim +- **THEN** its labels are cut apart and the batch finishes using the advertised trim default + +#### Scenario: No finishing option with none default +- **WHEN** the client omits finishings and the current advertised default has been changed to none +- **THEN** mandatory precut occurs, followed by no inter-label or final batch cuts, honoring the advertised default + +#### Scenario: Explicit none overrides default +- **WHEN** the client explicitly requests none +- **THEN** mandatory precut remains, but no inter-label or final batch cuts are introduced by the trim default + +#### Scenario: Consistent default updates +- **WHEN** an operator sets the collection default to trim-after-job and later sets the enum default to none +- **THEN** each update republishes consistent enum/collection defaults, omitted jobs use the latest preset, and none has a no-value collection default + +#### Scenario: Chain-only is an explicit job choice +- **WHEN** an operator attempts to set ptouch-chain as the printer default +- **THEN** the update is rejected without changing the existing defaults, while an explicit ptouch-chain job remains supported + +### Requirement: Explicit continuation survives idle time + +On a healthy matching-tape printer, the server SHALL preserve a deliberately unfinished run across idle time and subsequent batches. A later batch's mandatory precut SHALL separate the preceding batch's held end without generating another discarded leader. Its own finishing preset SHALL determine its internal and final cuts. Cleanup SHALL NOT resend label content. + +#### Scenario: Idle gap between jobs +- **WHEN** an unfinished batch completes and another valid batch arrives after an idle gap +- **THEN** the open run is preserved without an idle cut or printer reset, and the next batch's precut separates the held end without discarded leader waste + +#### Scenario: Terminal trim job +- **WHEN** one or more unfinished batches are followed by a trim job +- **THEN** the server continues the same tape run, applies the new batch's precut and internal cuts, and finishes its last label without an extra blank label + +#### Scenario: Missing terminal job +- **WHEN** a terminal batch is never submitted, canceled, or rejected before printing +- **THEN** idle cleanup does not cut or reset the preceding healthy unfinished run + +#### Scenario: Consecutive batches without internal cuts +- **WHEN** two batches request none +- **THEN** labels remain joined within each batch, while the second batch's precut separates the first batch's strip without generating a discarded leader + +### Requirement: Failure does not override the finishing choice + +A detected output, raster decode, or printer-status failure SHALL fail the job without replaying content or inventing a final cut. Finalization SHALL succeed only after its requested terminal command is transferred and readiness confirmed. Ambiguous partial output SHALL block automatic continuation pending explicit recovery. Rejected or canceled jobs that sent no output SHALL preserve a prior healthy run. + +#### Scenario: No-cut job canceled during printing +- **WHEN** a no-label-cuts job is canceled after printing starts +- **THEN** cleanup introduces no final cut and uncertain buffered output is not silently reused by later work + +#### Scenario: Terminal transfer fails +- **WHEN** the requested final cut cannot be confirmed +- **THEN** the job reports failure and repeated cleanup does not resend terminal commands or label content + +#### Scenario: Rejected follower +- **WHEN** a subsequent batch is rejected before sending any page setup or raster +- **THEN** it neither precuts nor damages the prior healthy unfinished run + +#### Scenario: Truncated final label +- **WHEN** a streamed batch ends with an incomplete raster label +- **THEN** the job aborts, the incomplete label receives no normal print/feed or final-cut command, and uncertain output requires explicit recovery + +### Requirement: Existing rendering is preserved + +The server SHALL retain 1:1 default printing, strict tape-width rejection, and offline media recovery as defined by label-rendering and printer-device. The finishing choice SHALL NOT change rendered pixels or page dimensions. Advertised formats and copy limits SHALL describe executable behavior. Changed physical printing and cutting SHALL be verified on the PT-2730 before claiming hardware success. + +#### Scenario: Copies +- **WHEN** a valid PNG requests copies within its advertised supported range +- **THEN** all requested copies use the resolved finishing policy and the same image geometry + +#### Scenario: Offline printer returns +- **WHEN** queued jobs resume after a printer returns with matching tape +- **THEN** media is confirmed before printing and each job retains its requested finishing choice + +## ADDED Requirements + +### Requirement: Actual pages determine label boundaries + +One IPP job SHALL define one batch. PNG copies and actual PWG raster or URF labels SHALL receive the selected batch policy without treating copies as document pages. Successful end of input SHALL determine the final label even when the total count is omitted. The server SHALL distinguish normal completion from abort cleanup and never infer batches from queued jobs or idle time. + +#### Scenario: Two-page raster with one copy +- **WHEN** a valid two-page PWG raster job requests one copy and trim +- **THEN** both labels print in one chain, their shared boundary is cut, the final label is cut free, and the job completes successfully + +#### Scenario: Unknown page count +- **WHEN** a valid streaming raster job omits its total page count +- **THEN** every complete label uses the requested policy, with the final feed/cut decided at successful end of the batch, without requiring custom count metadata + +#### Scenario: No-cut multi-page job +- **WHEN** a valid multi-page job explicitly requests none +- **THEN** precut prepares only the first label, the labels remain joined, and the batch stays open for explicit continuation + +### Requirement: Precut is mandatory batch preparation + +The server SHALL prepare the first label of every nonempty batch with precut, including no-label-cuts and final-only batches. Precut SHALL NOT enable cuts between later labels when that preset forbids them. A healthy continuation SHALL use the existing tape boundary without producing discarded leaders between batches. A new closed run can require one initial leader. + +#### Scenario: Fresh batch +- **WHEN** a nonempty batch begins on a closed run with any preset +- **THEN** its initial leader is cut off before the first label + +#### Scenario: Continuing batch +- **WHEN** a nonempty batch follows a healthy unfinished batch +- **THEN** precut separates the prior held end without creating a discarded inter-batch leader + +#### Scenario: Mandatory precut does not imply cut-each +- **WHEN** a three-copy PNG requests none or trim-after-job +- **THEN** precut applies to the first copy only and no cuts are introduced between the copies + +### Requirement: Finishing presets use truthful IPP meanings + +The server SHALL advertise none (3), trim (11), trim-after-pages (60), and trim-after-job (63). `trim` and 60 select cuts between labels plus the final cut; 63 selects only the final cut. Non-none enums SHALL have matching templates; collection no-value SHALL select none. Chain-only SHALL use the advertised implementation-defined `ptouch-chain` template through standard finishings-col. No private job attribute SHALL supply an independent final-cut switch. + +#### Scenario: Registered selections +- **WHEN** a client selects none by enum or collection no-value, or trim-after-pages or trim-after-job by enum or template +- **THEN** it receives that advertised policy without silently collapsing the selection to none or trim + +#### Scenario: Chain-only selection +- **WHEN** a client selects finishing-template ptouch-chain through finishings-col +- **THEN** cuts occur between labels while the final label remains attached + +#### Scenario: Redundant registered combination +- **WHEN** a client requests finishings 60 and 63 together +- **THEN** all labels are cut apart with one final batch cut, without a duplicate terminal cut or a claim that these values are independent switches + +#### Scenario: None alongside another finishing +- **WHEN** a client supplies enum none alongside supported finishing values +- **THEN** none is ignored as required by IPP and the remaining values determine the selected preset + +#### Scenario: Conflicting or unsupported selection +- **WHEN** a client requests an unsupported preset, a conflicting preset combination, or both finishings and finishings-col +- **THEN** the request is rejected with an appropriate standard IPP error before any print output diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/specs/label-rendering/spec.md b/openspec/changes/archive/2026-10-13-simplify-print-server/specs/label-rendering/spec.md new file mode 100644 index 0000000..5d976e0 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/specs/label-rendering/spec.md @@ -0,0 +1,95 @@ +# Spec Delta + +## Purpose + +Let ordinary CUPS and IPP clients print labels using standard document formats, media dimensions, and truthful capabilities without silent resizing or content loss. + +## ADDED Requirements + +### Requirement: Explicit media geometry is authoritative + +For image jobs, the server SHALL honor an explicitly selected IPP media size, media-col.media-size-name, or media-col dimensions. PNG auto-length SHALL apply only when the submitted attributes omit a concrete media length. A source-only or margin-only media-col SHALL NOT disable that fallback. + +#### Scenario: Explicit PNG length +- **WHEN** a 180-pixel-long PNG at 180 dpi requests 50 mm media length +- **THEN** the printed page remains 50 mm long rather than being replaced with 25.4 mm + +#### Scenario: PNG without length +- **WHEN** a valid PNG omits concrete media length +- **THEN** its length follows the along-tape pixel dimension and DPI, within the existing 5 to 300 mm limits + +#### Scenario: Collection without dimensions +- **WHEN** a valid PNG supplies media-col with only a source or margins +- **THEN** image-derived length remains available + +### Requirement: Raster pages retain their submitted geometry + +The server SHALL use each PWG raster or URF page's submitted dimensions and resolution, validating them against supported media and the loaded tape. It SHALL NOT replace those dimensions with the default page, or silently pad or discard raster lines. Contradictory explicit job and page geometry SHALL fail visibly. + +#### Scenario: Short raster page +- **WHEN** a valid raster page is 25.4 mm long and omits explicit IPP length +- **THEN** exactly that page length prints without padding to the 100 mm default + +#### Scenario: Bit-depth independence +- **WHEN** equivalent supported 1-bit and 8-bit raster pages are submitted +- **THEN** conversion preserves their dimensions and content placement + +### Requirement: Incomplete raster input fails visibly + +The server SHALL reject short raster rows, incomplete declared page data, and malformed stream termination rather than completing the job with missing content. It SHALL withhold the affected label's normal print/feed or final-cut command, report job failure, and require recovery when already-transferred output is uncertain. Already printed labels SHALL NOT be replayed. + +#### Scenario: Partial final raster label +- **WHEN** a raster stream stops before supplying all of its final label's declared rows +- **THEN** the job aborts and the partial label receives no normal finishing command + +#### Scenario: Short row or malformed next header +- **WHEN** a raster row is incomplete or the next header is malformed +- **THEN** decode failure is not interpreted as successful end of the batch + +### Requirement: Default 1:1 PNG rendering does not discard image content + +With print-scaling none, the server SHALL reject PNG content that cannot fit the imageable area before sending page raster. It SHALL accept an imageable-area PNG that fits, or a nominal-width PNG canvas whose non-imageable margins are blank. It SHALL honor explicit orientation and retain the documented DPI fallback for PNGs without resolution metadata. + +#### Scenario: Oversized source image +- **WHEN** a portrait 200 by 300 pixel PNG at 180 dpi is sent to 12 mm tape with print-scaling none +- **THEN** the job fails with a useful geometry error and no page raster is emitted + +#### Scenario: Nominal-width canvas +- **WHEN** a correctly sized nominal-width image contains only blank pixels outside the tape's imageable window +- **THEN** its printable content is retained at the requested physical size + +#### Scenario: Explicit page too short +- **WHEN** a PNG at 1:1 exceeds the explicitly requested page length +- **THEN** it is rejected rather than cropped + +### Requirement: Advertised tape media is usable + +The server SHALL advertise supported nominal tape widths and publish their actual imageable margins through media-ready and media-default after confirmation. Every advertised tape width SHALL be accepted by media publication and printing. Geometry validation SHALL use the physical tape's imageable window, rather than a different tape's cached margins. + +#### Scenario: All advertised widths +- **WHEN** each advertised 6, 9, 12, 18, or 24 mm tape is reported loaded +- **THEN** media publication succeeds with that width and its correct margins + +#### Scenario: Wrong tape +- **WHEN** a submitted page width differs from the loaded tape +- **THEN** the job fails before its raster reaches the printer + +#### Scenario: CUPS custom-size rounding +- **WHEN** CUPS rounds a custom nominal tape width by at most 0.01 mm +- **THEN** the advertised width range accepts the request while preserving its submitted geometry, explicit margins, and physical imageable window + +#### Scenario: Conservative CUPS custom margins +- **WHEN** a custom-size request contains larger explicit margins than the tape requires +- **THEN** fitting content prints at 1:1 and oversized content fails before raster, without replacing the margins or shrinking the image + +### Requirement: Capabilities describe executable inputs + +The server SHALL advertise only printable public document formats, with no private filter destination masquerading as an input. It SHALL honor its advertised format-specific copies range; PNG copies SHALL support 1 through 999, and streaming raster SHALL advertise one copy with multiple document pages supported. + +#### Scenario: Format query +- **WHEN** a client queries document-format-supported +- **THEN** supported standard formats remain available and image/vnd.ptouch-raster is absent + +#### Scenario: Former planner limit +- **WHEN** a valid PNG requests 65 copies +- **THEN** all 65 copies print without a planner-capacity failure diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/specs/printer-device/spec.md b/openspec/changes/archive/2026-10-13-simplify-print-server/specs/printer-device/spec.md new file mode 100644 index 0000000..ac63af1 --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/specs/printer-device/spec.md @@ -0,0 +1,75 @@ +# Spec Delta + +## Purpose + +Expose one stable IPP queue for verified hardware, select its USB device predictably, and report availability and faults accurately across printer power cycles. + +## ADDED Requirements + +### Requirement: Only supported hardware receives print output + +The server SHALL create operational queues only for implemented and verified model paths, initially PT-2730. Recognizing a USB ID SHALL NOT grant print support. Invalid or unsupported model configuration SHALL fail explicitly rather than fall back silently. + +#### Scenario: Unsupported Brother attached +- **WHEN** discovery finds a model without an implemented print path +- **THEN** no PT-2730 raster is sent to that model and the unsupported device is identified in diagnostics + +#### Scenario: Invalid configured model +- **WHEN** the configured model is unknown or unsupported +- **THEN** startup fails with a useful configuration error + +### Requirement: Device selection is unambiguous + +The server SHALL expose one application queue and consistently select its configured supported model. Multiple matching physical devices SHALL be reported as ambiguous; discovery or opening SHALL NOT silently choose the first match. USB renumbering of a single matching device SHALL NOT require queue replacement. + +#### Scenario: Two matching devices +- **WHEN** two PT-2730 units match the configured queue +- **THEN** printing is blocked with an ambiguity diagnostic + +#### Scenario: Device replug +- **WHEN** the sole matching printer is unplugged and reattached with another USB node number +- **THEN** the existing queue recovers without selecting a different model or requiring a server restart + +### Requirement: Availability and faults are reported truthfully + +The server SHALL retain the queue while hardware is absent and report standard IPP reasons for unavailability, missing tape, and printer faults. USB presence alone SHALL NOT establish readiness, and parsed printer error bits SHALL be checked before page raster output. Test transports SHALL NOT invoke USB operations on incompatible connection data. + +#### Scenario: Present device cannot open +- **WHEN** USB enumeration finds the printer but the server cannot claim it +- **THEN** the queue does not report the printer as ready merely because USB is present + +#### Scenario: Fault with recognized tape +- **WHEN** status reports a recognized tape and a printer error +- **THEN** that error is exposed and new page raster is withheld + +#### Scenario: Socket test transport +- **WHEN** a socket sink is configured while Brother USB hardware is present +- **THEN** status processing never passes the socket connection to USB-specific operations + +### Requirement: Offline recovery confirms media before printing + +The server SHALL accept queued work while the printer is off and resume valid work after it returns. Image page geometry derived from loaded media SHALL be constructed only after live media confirmation. Explicit raster page geometry SHALL be validated against the confirmed tape rather than silently changed to match it. + +#### Scenario: Start while off +- **WHEN** a PNG without explicit media geometry is queued while the server starts with the printer off, then the printer returns with supported tape +- **THEN** geometry uses that confirmed tape and the job can complete without a server restart + +#### Scenario: Existing unfinished run +- **WHEN** a healthy chain-only or no-label-cuts run remains open during status polling or a subsequent valid job +- **THEN** cassette detection continues without a printer reset that destroys the unfinished run + +### Requirement: Uncertain output requires explicit recovery + +When interrupted output leaves the printer's buffered label or final-cut result uncertain, the server SHALL report recovery required and block new raster output. It SHALL NOT blindly reset, print, cut, or append to that buffer. The documented conservative recovery SHALL power-cycle the printer and then restart the server. A clean status response or USB reopen alone SHALL NOT clear the uncertainty. + +#### Scenario: Interrupted label followed by another job +- **WHEN** raster transfer fails partway through a label and another job arrives +- **THEN** the next job emits no raster or automatic reset and the queue reports the required recovery action + +#### Scenario: Explicit recovery +- **WHEN** the operator power-cycles the printer and restarts the server after uncertain output +- **THEN** a new job confirms media and starts a fresh run without replaying the interrupted content + +#### Scenario: Rejected job before output +- **WHEN** validation rejects a job before page setup or raster is sent +- **THEN** a previously healthy unfinished run is not reclassified as uncertain merely because the job was rejected diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/tasks.md b/openspec/changes/archive/2026-10-13-simplify-print-server/tasks.md new file mode 100644 index 0000000..943e7fa --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/tasks.md @@ -0,0 +1,64 @@ +# Tasks + +## 1. Establish the required build and CI checks + +- [x] 1.1 Add an explicit CMake core-only mode and require dependencies for the full app; verify core-only works without PAPPL, missing full-app dependencies fail clearly, and the complete container build includes the adapter tests. +- [x] 1.2 Run the existing core, driver, and capture suites in CI for PR and main before image publication; verify a deliberately failing local test prevents the publish stage and Python capture tests cannot be silently omitted. +- [x] 1.3 Update README build/test commands for both modes and review them against the actual CMake and CI invocations. + +## 2. Preserve standard raster geometry and propagate input failures + +- [x] 2.1 Add IPP/socket regressions for short 1-bit and 8-bit PWG/URF pages without explicit length; verify current code pads or replaces geometry and retain captured line-count/content expectations for the corrected behavior. +- [x] 2.2 Implement the bounded pinned-PAPPL raster patch and apply it identically in development and deploy image builds; verify both bit depths preserve each page's geometry, conversion buffers retain the correct type and size, and the build fails if the patch cannot apply cleanly. +- [x] 2.3 Validate malformed headers and contradictory explicit job/page dimensions before page raster; verify rejection with no page output, correct conversion buffer sizes, and no default-length padding or truncation. +- [x] 2.4 Make short raster rows, incomplete pages, and malformed next headers fail visibly instead of becoming clean end-of-input; add the approved bounded CUPS decoder correction and PAPPL error propagation, apply compatible pinned source/security corrections identically in both image builds, and ship the corrected runtime libraries. Verify both supported bit depths abort without printing or finalizing an incomplete label, including the raster-discard path, while valid unknown-count streams complete. +- [x] 2.5 Check write-callback failures immediately and preserve failure through image and streaming cleanup; verify no further raster or pending print command follows a failed callback, and repeated end-job callbacks cannot replay output or turn an aborted job into success. +- [x] 2.6 Document the dependency patches' causes, bounded CUPS/PAPPL raster and finishing scope, pinned versions/security correction policy, and removal conditions beside the patches; review that no new raster parser, page spooler, or generic finishing engine was introduced. + +## 3. Give the queue state an owner and enforce device readiness + +- [x] 3.1 Move closed/healthy-open/uncertain run state and media confirmation into driver-managed printer extension data and retain necessary locking; verify idle polling, cassette swaps, printer deletion, and healthy unfinished-run continuation without a fixed slot registry, preserving the existing queue URI and PAPPL flag needed for multiple pending jobs. +- [x] 3.2 Validate the configured model before discovery and permit only the implemented PT-2730 print path; verify unsupported/unknown configuration fails clearly and recognized unsupported devices receive no print output. +- [x] 3.3 Count matching devices at discovery/open and block ambiguous selection; verify injected zero/one/two-device enumerations and replug recovery of the single supported device without relying on USB enumeration order. +- [x] 3.4 Centralize transport validation and safe USB reads, with a defined socket-test status path; verify a socket fixture plus injected Brother presence cannot reach USB-specific connection access. +- [x] 3.5 Consolidate tape/error status publication and bounded job refresh; verify absent, unclaimable, empty, malformed, unsupported-media, and error-bit cases do not report ready or emit page raster, while valid offline work resumes. +- [x] 3.6 Block new output after an ambiguous partial transfer or failed print/readiness confirmation; verify clean status and USB reopen do not clear uncertain state, no automatic reset or appended frame is emitted, and failures before output preserve a healthy open run. +- [x] 3.7 Document and verify explicit uncertain-output recovery by printer power-cycle followed by server restart; review README and ADR-0002/0007 against supported-device, offline, state-ownership, and unfinished-run restart behavior, and explain how to inspect failed jobs and resubmit without promising queue persistence or replay. + +## 4. Make media and image rendering predictable + +- [x] 4.1 Introduce the shared canonical tape-geometry calculation and fix the printer-wide margin bound; verify ready/default publication and imageable windows for 6, 9, 12, 18, and 24 mm, plus wrong-tape rejection before raster. +- [x] 4.2 Detect concrete submitted media length and retain it through PNG rendering; verify named media and media-col dimensions preserve a requested 50 mm page, while omitted length and source-only/margin-only collections retain PNG auto-length. +- [x] 4.3 Reject oversized 1:1 PNG source content and too-short explicit pages; verify the 200-by-300 PNG emits no page raster, fitting imageable designs retain markers, and nominal-width PNG canvases require blank non-imageable margins. +- [x] 4.4 Preserve documented orientation and DPI behavior through the new geometry path; verify asymmetric portrait/landscape markers, explicit orientation, no-DPI PNGs, non-square DPI rejection, and existing minimum/maximum length boundaries. +- [x] 4.5 Replace the private MIME route with actual image/png driver format, PNG-to-PNG callback registration, and a real PNG printfile wrapper; verify real IPP uploads select the owned path, image copies are expanded exactly once, failure cleanup is idempotent, and supported formats contain PNG once with no private raster MIME. +- [x] 4.6 Check explicit geometry across PNG, JPEG, and standard raster and reconcile advertised scaling with implemented behavior; verify no silently substituted size/scaling policy, while documenting PNG-only auto-length when geometry is omitted. +- [x] 4.7 Update README printing guidance and ADR-0004/0005/0006 for geometry precedence, printable-area checks, margins, and MIME routing; review examples against the capture regressions. + +## 5. Replace the copy planner with four batch finishing presets + +- [x] 5.1 Establish an authorized bounded hardware protocol gate before enabling the new finishing path: derive command fixtures and run the approved eight-label gate (chain:2 then each:2, and none:2 then end:2), especially late chain-off before final EJECT and final-only cutting with automatic between-label cutting disabled; retain one-label/three-label coverage in captures; verify the candidate also supports continuation without discarded leaders. If the candidate fails, revise the command policy and repeat the gate rather than dropping a required mode or requiring total-label metadata. +- [x] 5.2 Add IPP regressions for two-label and unknown-count raster batches, multiple label lengths, three PNG copies, all four modes, both finishing representations, and omitted defaults; verify one job is one batch, copies and actual raster labels are distinguished, and the old aborted second label is exposed. +- [x] 5.3 Define one driver-owned preset table for chain-only, chain-and-final, no label cuts, and final-only; map them to the implementation-defined finishings-col template ptouch-chain, trim-after-pages (60) with trim (11) compatibility alias, none (3), and trim-after-job (63). Implement the bounded pinned-PAPPL advertising, validation, and default-management bridge; verify raw submitted attributes and the current advertised defaults resolve without relying on PAPPL's lossy finishing bitmask, attribute overrides are unambiguous, and legacy drivers retain their behavior. +- [x] 5.4 Advertise truthful finishings-supported/ready/default and collection templates/database/default; verify every advertised preset is actually accepted, 60 plus 63 produces one chain-and-final operation, enum none alongside other values is ignored per IPP, unsupported/conflicting selections and simultaneous enum/collection job requests are rejected, and no new job attribute or private enum is introduced. Keep one canonical printer default limited to presets with enum equivalents: initialize matching trim defaults, verify a collection update to trim-after-job followed by an enum update to none synchronizes both representations and omitted-job behavior, publish collection no-value for none, and reject chain-only or contradictory default updates without changing the current default. +- [x] 5.5 Replace the copy-sized operation array/scans with small per-job state and one deferred print/feed command: the next valid label releases the preceding command, and successful end-job releases the final command according to the selected preset. Verify unknown label counts, fragmented receive-ready notifications, and one-label batches without buffering another raster image or fully feeding/ejecting every label. +- [x] 5.6 Make precut mandatory exactly once at the first actual label of each nonempty batch and between-label cutting depend only on the chosen preset; verify three PNG copies cannot retrigger precut despite PAPPL passing page index zero for every copy, and fix the pure cutter helper's unconditional per-label precut behavior. +- [x] 5.7 Latch all transfer, decode, readiness, and cancellation failures through image and streaming callbacks before cleanup; verify failed raster/FF/EJECT aborts without subsequent raster, pending print commands, final cuts, or content replay, and repeated cleanup emits nothing. Keep healthy-open state only after confirmed continuation and block ambiguous output using the recovery policy from section 3. +- [x] 5.8 Remove the planner's copy ceiling and retire its operation-array APIs while retaining the required chain and final-only semantics; adapt hardware-free tests and cut probes to observable command policy, verify 65 and 999 PNG copies complete at a counting sink, and keep streaming raster's advertised copy count at one. +- [x] 5.9 Verify the finishing transition matrix, including healthy open runs across idle gaps, rejected/canceled followers, and cassette changes; verify the next batch's mandatory precut releases the previous held end without a discarded leader, none/none batches become separate uncut strips, and no ESC @ reset occurs while a healthy unfinished run remains open. +- [x] 5.10 Update README cutting examples, ADR-0008, and finishing/probe documentation for all four presets, mandatory precut, trim-by-default, and efficient batch chaining; explain the implementation-defined meaning of ptouch-chain, changed none/none strip boundaries, and migration of cached CUPS defaults through standard queue settings. Claim physical leader behavior only after the hardware gates pass. + +## 6. Remove avoidable operational machinery + +- [x] 6.1 Switch PAPPL to normal-level stderr logging and remove the debug logfile/tail process; verify container logs contain startup/job/error diagnostics and the container no longer needs a log file follower. +- [x] 6.2 Remove the ineffective width-policy knob and PTOUCH_PRECUT from supported configuration and example deployment, and document retirement of obsolete cut-mode settings; verify strict geometry rejection and mandatory batch precut remain intrinsic and no replacement server knob is added. +- [x] 6.3 Extend the existing image smoke with real IPP printing into an isolated socket sink; verify startup, truthful attributes, dimensions, all advertised finishing presets and defaults, and failure propagation without USB access. +- [x] 6.4 Update deployment and logging documentation while retaining the tested USB bind mount and dbus/Avahi arrangement; verify examples match the built image and dependency startup failures remain visible. + +## 7. Validate the complete client and hardware contract + +- [x] 7.1 Run the required full CMake build and CTest checks in the container, the hardware-free build/capture checks, and all new IPP regressions; verify results distinguish core, adapter, and deployment coverage with no omitted suites. +- [x] 7.2 Exercise a conventional driverless CUPS queue against the isolated candidate server using lp for PNG/JPEG and a multi-label document rendered through CUPS; verify explicit geometry, default finishing, registered 60/63 submissions, and at least two valid jobs queued offline and resumed. Exercise ptouch-chain through standard finishings-col submissions and document client/UI limitations of its implementation-defined name. Run each of the four presets with one PNG label, three PNG copies, and three unknown-count raster labels, plus truncated input and output failures at each command stage. +- [x] 7.3 Run an authorized bounded PT-2730 hardware acceptance matrix: chain-only batches of three and two labels separated by an idle gap, then a chain-and-final batch of one; none batches of two and two labels, then a final-only batch; separate three-label chain-and-final PNG and raster batches; and a three-copy final-only strip. Check label separation, one mandatory precut per batch, no discarded leaders between healthy continued batches, and expected initial leaders for fresh runs. Include length/orientation markers, cassette changes, power-cycle/offline queuing, available tape widths, and uncertain-output blocking followed by explicit recovery without ghost output; record physical evidence separately from command captures. +- [x] 7.4 Review the complete source/doc/spec changes against all audit findings and the simplification goals; verify no copy-sized planner, global slot registry, private MIME advertisement, log-tail process, optional precut setting, unsafe socket/USB cast, or unsupported-model print path remains, and retain an explicit explanation for the bounded PAPPL raster/finishing patch and conservative uncertain-output recovery. +- [x] 7.5 After implementation and required validation are complete, sync the delta specifications, run openspec validate --all --strict, and archive simplify-print-server; verify all tasks and validation evidence are complete before archival, and leave publishing/deployment to a separate user instruction. diff --git a/openspec/changes/archive/2026-10-13-simplify-print-server/validation.md b/openspec/changes/archive/2026-10-13-simplify-print-server/validation.md new file mode 100644 index 0000000..661715a --- /dev/null +++ b/openspec/changes/archive/2026-10-13-simplify-print-server/validation.md @@ -0,0 +1,583 @@ +# Current validation status + +[verified] All 42 tasks are complete. The three specifications are synced and +strict validation passes. The change was archived locally on 2026-10-13. + +The authorized 21-label integrated PT-2730 acceptance passed on 12 mm tape, +including idle/offline continuation, cassette change, truncated-input blocking, +and explicit recovery without replay. The earlier eight-label direct-protocol +24 mm gate also passed. Other supported widths retain software-only coverage. +The original deployed service was restored on its unchanged image. No further +printing or deployment is authorized. + +Claude's independent implementation review is retained in +[evidence/claude-implementation-review.txt](evidence/claude-implementation-review.txt). +The physical session's requests, fixtures, logs, USB capture, and status packets +are preserved in [evidence/ptouch-integrated-12mm.tar.gz](evidence/ptouch-integrated-12mm.tar.gz), +without compiled tools or the candidate container image. Entries below record +historical checkpoints; their pending-task language describes that checkpoint, +not the current physical acceptance status. Final review fixes and their fresh +software verification are recorded at the end before archival. + +# Validation history + +Work remains on `refactor/simplify-print-server`. No commits were pushed and +the deployed image has not been replaced. Initial software validation had no USB +access; the subsequently approved eight-label gate preparation is recorded below. + +## Verified foundation + +- Explicit core-only builds run six CTest suites without PAPPL discovery. + Full builds require PAPPL, libusb, libpng, and Python; missing dependencies + fail configuration instead of silently omitting the app or capture tests. +- Both PR and main CI run the deploy build stage before publication. A temporary + deliberately failing CTest stopped the build before application installation + and runtime-image creation. +- Fake USB enumeration covers zero, one, and two matching devices, unsupported + devices, changing USB nodes, and claim failures. Configuration and driver + registration reject unknown or unsupported model identities. +- Real PAPPL socket connections and injected Brother presence exercise the + status transport guard without USB operations or opaque-data access. +- A real isolated server completed a PNG job and aborted a malformed PNG job, + with startup, job, completion, and error messages on stderr. Injected dbus and + Avahi startup failures stopped the entrypoint before starting the application. + +Checks ran in local containers with repository source mounted read-only, no USB +mount, and no external network during the test runs. The host has no CMake; +container builds provide both core and adapter coverage. Build/CI, device +selection, and logging changes received a read-only review; its probe URI +assignment finding was fixed and both build modes passed afterward. + +## Raster boundary correction + +The PAPPL patch and its independent IPP/socket fixture cover submitted geometry, +conversion, malformed headers, media conflicts, incomplete rows, and callback +failure cleanup. This fixture uses a minimal driver to isolate the dependency +boundary; it does not certify the Brother finishing sequence. + +The initial checkpoint passed nine CTest suites with 20 real IPP raster/image +cases. Expanded coverage now includes malformed stream termination as required +coverage. Patch application succeeds on pinned source and fails for duplicate +application or corrupted context. Tasks 2.1, 2.2, 2.3, and 2.5 are complete; +task 2.6 stays open until the finishing bridge is also documented. + +See [patch documentation](../../../../patches/README.md) for pinned dependencies, +security correction policy, scope, and removal conditions. + +## Approved scope extension + +The original regression appended one byte after a complete URF label and still +produced a completed job and successful end-job callback. CUPS 2.4.x reported a +short next header like clean EOF; its private compressed-input buffer prevented +a reliable PAPPL-side byte-count check. + +The user approved a bounded CUPS decoder correction that reports +partial-header failure through its existing error mechanism, plus PAPPL error +propagation before normal end-job. The proposal, design, and tasks include that +extension beyond PAPPL-only dependency patches. Both development and deploy +images now build Debian's security-patched CUPS source through the shared helper; +runtime checks show PAPPL and libcupsimage resolve the patched libcups. Required +regressions also cover negative input callbacks for PWG, URF, and uncompressed +raster. Task 2.4 is complete: the rebuilt deploy image and optimized Release +build pass all ten suites, and the final runtime IPP smoke and library/source +version checks pass. Captured Release flags retain -UNDEBUG after -DNDEBUG. + +## Geometry, PNG, and printer ownership + +- All five tape widths share canonical nominal/imageable geometry. Publication + and placement regressions cover their pixel windows, including 18 mm margins, + wrong tape, and unsafe requested margins before page output. +- Real IPP PNG/JPEG jobs cover explicit 50 mm media and PNG-only auto-length, + named media and media collections, nominal blank margins, oversized content, + orientation markers, DPI, length boundaries, copies, and standard scaling. + PNG is routed as image/png with no private advertised format. Malformed image + and transfer failures emit no subsequent raster or finishing command. +- Printer-owned extension data replaces the eight-slot global registry. Fake + status and transport tests cover cassette changes, unavailable/error states, + failed output confirmation, and healthy rejected/canceled followers. Clean + status or USB reopen cannot clear uncertain output; recovery is documented as + printer power-cycle followed by server restart. Physical recovery is pending. + +A read-only review of the completed state/status/geometry and PNG source plus +behavioral assertions found no actionable regressions. Pending finishing work +was explicitly excluded from that conclusion. + +## Test fixture correction + +An attempted HTTP connection shortcut to wake PAPPL at shutdown introduced a +detached-client lifetime hazard: joining the system thread does not join that +client before system deletion. The shortcut was removed from all three fixtures. +Tests retain PAPPL's ordinary shutdown polling. Test-only -UNDEBUG keeps assertion +checks and fixture setup active in Release; production compiler flags are +unchanged. All six core-only suites pass in Release with assertions enabled. +Final full image verification passes. Five repeated driver runs and five repeated +PNG runs also pass in Release with assertions enabled, taking 299.91 seconds; +`ctest-repeat-fixed.log` records this additional teardown check. + +One earlier default-build crash has no retained compiler flags or backtrace, so +its exact cause is unproven. Separately reproduced failures establish disabled +Release fixture assertions and intermittent heap corruption with the shutdown +shortcut. No useful sanitizer stack trace was obtained. + +At this earlier checkpoint, local evidence was retained under +`/tmp/ptouch-pappl-raster-results/`, `/tmp/ptouch-cups-raster-results/`, and +`/tmp/ptouch-driver-crash-results/`. +The final dependency/build evidence is `docker-deploy-build-final-verified.log`, +`release-ctest.log`, `release-driver-flags.make`, `release-raster-flags.make`, and +the `runtime-*-final.log` files in the CUPS results directory. Older failing logs +are retained as historical debugging evidence, not the final verification. + +## Hardware gate + +The test-only batch probe and 11 command-capture cases cover the proposed four +modes, first-label precut, deferred terminal command, and multi-batch transitions. +The user approved a reduced initial gate of eight labels on 24 mm tape: +chain:2 followed by each:2, and none:2 followed by end:2. Framed raster totals +223.52 mm, plus two initial leaders and feed margins. The earlier 27-label +proposal is superseded; the larger integrated acceptance matrix is separate. See +[probe instructions](../../../../tests/CUT-PROBE.md) for exact commands, expected +cuts, continuation checks, and failure stop conditions. + +The four-preset driver lifecycle and PT-2730 hardware gates remain pending. +Command captures and socket jobs do not establish physical cuts or leader waste. + +### Eight-label gate preparation + +[verified] On vault, the existing ptouch queue was idle with zero jobs. Its +server was temporarily stopped for a direct status-only query, then restored. +The query emitted no reset, raster, feed, or cut. The reply was type 00, phase 00, +error 0000, with cassette width 0 mm: +`8020423063300000000000000000000000000000000000000000000000000000`. +No physical labels were sent. Load/confirm the 24 mm cassette and power-cycle +before continuing. The probe, status log, and future captures are in +`vault:/tmp/ptouch-eight-label-gate.MDnnuO/`. + +The exact eight-label policy was separately captured without USB: both pairs +contain four labels, one initialization, one EJECT, no empty terminal command, +and the expected first/later autocut flags. The current compatible runtime +executes the transferred probe successfully. + +### Initial gate, stage 1: two chain-only labels + +[verified] After the operator reseated the cassette and power-cycled, a direct +status query reported clean 24 mm tape: +`8020423063300000000018010000000000000000000000000000000000000000`. +The original server was then stopped for the approved physical test. The probe +transferred exactly two chain-only labels (7,564 bytes); each received the +printing/completion/receiving-ready sequence with zero error bits. Captures show +180/216 rows, two FF commands, one initialization, and no EJECT. + +Evidence: `vault:/tmp/ptouch-eight-label-gate.MDnnuO/chain-2.{prn,log}` and +`status-after-power-cycle.log`. The server remains temporarily stopped to avoid +resetting the healthy held run during physical inspection. Two of eight labels +have been sent; leader count, cut separation, and held end await operator +confirmation. Task 5.1 remains unchecked, and the production finishing path is +unchanged. + +The operator reported `leader-cut-rectangle-cut-rectangle`, matching the +expected initial leader and cut between the two labels, with no trailing cut +reported. The following chain-and-final batch is being tested as continuation +of this same held run. + +### Initial gate, stage 2: two chain-and-final labels + +[verified] The continuation probe transferred 7,566 bytes and received clean +completion/receiving-ready notifications for both labels. Its capture contains +180/216 rows, intermediate FF, late chain-off and final EJECT, and no ESC @ +initialization. Evidence: `vault:/tmp/ptouch-eight-label-gate.MDnnuO/each-2.{prn,log}`. + +The operator reported `leader-cut-label-cut-label-cut`, then clarified that +"leader" meant the last rectangle held from the first batch, not blank scrap. +This confirms the prior held label was released, both new labels were cut, +the final batch cut succeeded, and no inter-batch leader was discarded. Four +of eight labels have been sent. The original server was restored after the +confirmed final cut. Both zero-error receiving-ready and the operator's final +cut confirmation establish a closed run for the next fresh initialization; +another manual power-cycle is unnecessary here. Task 5.1 remains unchecked +until the no-cut/final-only pair is physically verified. + +### Initial gate, stage 3: two no-cut labels + +[verified] The idle restored queue had zero jobs; a direct status query confirmed +clean 24 mm tape before the server yielded USB access. The no-cut probe then +transferred 7,564 bytes and received clean completion/receiving-ready for both +labels. Captures contain 180/216 rows, first-label autocut on, later-label autocut +off, two FF commands, and no EJECT. A fresh initialization followed the preceding +physically confirmed final cut, rather than resetting an unfinished run. + +Evidence: `vault:/tmp/ptouch-eight-label-gate.MDnnuO/none-2.{prn,log}` and +`status-before-none.log`. Six of eight labels have been sent. The original server +remains temporarily stopped during inspection of the held strip; physical +confirmation of one leader and an uncut two-label strip is pending. + +The operator reported `leader-label`, with the second rectangle potentially +inside the print head. No intermediate cut was reported; the joined strip will +be inspected fully when the following batch's precut releases it. + +### Initial gate, stage 4: two final-only labels + +[verified] The continuation probe transferred 7,566 bytes, with clean completion +and receiving-ready for both labels. Captures show 180/216 rows, first-label +autocut on and later-label autocut off, intermediate FF, late chain-off and final +EJECT, and no ESC @ initialization. Evidence: +`vault:/tmp/ptouch-eight-label-gate.MDnnuO/end-2.{prn,log}`. + +The operator reported `finished double label from last turn-cut-double label-cut`. +This confirms the no-cut batch was joined, its held end was released by the next +batch, the final-only batch was joined and cut free, and no new blank scrap was +reported between them. All eight authorized labels have been sent. The original +server is running again; its image/configuration was not replaced. + +The operator subsequently confirmed the no-cut batch's fresh-run blank leader +was cut off. The eight-label initial gate passes all four cutting policies, +mandatory first-label precut including none, late chain-off/final EJECT, and +healthy continued batches without discarded leaders. Task 5.1 is complete. +This evidence covers these bounded two-label direct-protocol cases on 24 mm +tape; the integrated PNG/raster, media/recovery, and other-width acceptance +matrix is still pending. Actual tape length was not measured. No further labels +are authorized by this completed eight-label gate. + +## Completed batch implementation + +The Release driver matrix passes against the initial finishing SDK with test +assertions enabled. Tasks 5.2, 5.5–5.9 are complete: real IPP all-mode PNG1/3, +unknown-count mixed-length PWG1/8 and URF8, enum/template/no-value selections, +16 run transitions, deferred output and failure cleanup, mandatory first precut, +PNG65/999 counting sinks, and truthful raster copies=1. The retired precut/width +settings have no production read or replacement knob; task 6.2 is complete. + +Evidence: `/tmp/ptouch-driver-production-results/task-evidence.md` and +`driver-matrix-pass.log`, plus the pure cutter/core/capture results. The previous +full-suite log includes a corrected raster-copies test expectation and a pending +PAPPL public-default setter regression; it is not the final all-suite result. + +An ordinary driverless CUPS queue exposed a separate media-advertisement gap: +it submits 12x50 mm with the printer-wide 53/53 minimum margins, exceeding that +tape's physical window. The driver correctly rejects it without page output. +Task 7.2 remains open while canonical per-size media metadata is corrected and +the conventional queue rerun. The synchronized default/persistence bridge, +deploy-image smoke, final all-suite checks, and integrated physical acceptance +remain open. + +## Final finishing and conventional CUPS checks + +[verified] Tasks 5.3/5.4 pass the standalone real-IPP PAPPL fixture and Release +production driver matrix. Defaults synchronize across enum/collection updates, +the public setter, and saved-state reload; malformed/contradictory defaults leave +the previous value unchanged. Legacy fixture behavior is retained. The refreshed +SDK has canonical media overrides with one 20-entry database and five fixed-width +variable-length ranges. The driver/PNG suites pass 2/2, including partial second +labels in all four modes without terminal EJECT. Evidence: +`/tmp/ptouch-driver-production-results/ctest-final.log` and +`/tmp/ptouch-final-deploy-build.log` (PAPPL fixture passes; the first full run's +media test omitted the explicit database request and was corrected). + +[verified] Task 7.2 passes against a conventional driverless CUPS queue in an +isolated, network-disabled container without USB. `lp` submits PNG, JPEG, and +two-label PDFs at 12x50 mm with captured 354-row labels. Enum 60 separates labels +and cuts the last; enum 63 leaves the PDF labels joined and cuts the last. Two +valid jobs remain queued while the printer socket refuses connections, then +both complete after the socket becomes available. A direct standard collection +request selects `ptouch-chain` and leaves the last label held. All-mode +one/three-copy PNG and unknown-count raster plus input/output failures are +covered by the production driver matrix. Evidence: +`/tmp/ptouch-cups-queue-offline.log`; reproducible fixture: +`scripts/check-cups.py`. Ordinary CUPS queue settings use +`lpoptions -p -o finishings=11` to override a generated/cached explicit +none default; server-side explicit none remains honored. + +The final runtime/full-suite run and image smoke are still pending. A separate +Web UI GET truncation is under diagnosis; the new legacy-form guard preserves +canonical defaults in source but that form regression is not yet verified. + +[verified] The final source/doc/spec review found two guide references and a fixture +retry mismatch; all are corrected. Task 7.4 is complete. The retired mechanisms +remain absent, the bounded patches are explained beside their source, and strict +OpenSpec validation passes both current items. The guide now describes metadata +lookup separately from the explicit supported-model gate. + +[verified] The Web UI failure reproduces in the pre-finishing application too: +PAPPL's optional NULL footer is passed to localization and crashes the renderer. +The app now initializes an empty footer through the existing public API. The +reproducible CUPS fixture passes actual GET/POST Printing Defaults checks with +both enum 11 and enum 63 unchanged after an unrelated print-quality edit, followed +by all ordinary CUPS/offline/collection cases. Evidence: +`/tmp/ptouch-cups-web-final2.log`; baseline crash trace: +`/tmp/ptouch-driver-production-results/web-baseline-app.log`. + +The final image smoke administers defaults through the existing container-local +`ptouch-app modify` CLI, which sends standard Set-Printer-Attributes; it preserves +PAPPL's existing restriction on unauthenticated remote administrative operations. + +## Software completion + +[verified] All authorized software tasks are complete, 39 of 42 overall. The final +runtime build includes the empty-footer fix and passes all 11 CTest suites in +120.19 seconds. The finished image smoke passes all four presets, explicit 50 mm +geometry, three-copy PNG batches, healthy continuation, synchronized omitted +printer defaults, and malformed-image abort without printer output. Evidence: +`/tmp/ptouch-final-deploy-build3.log` and `/tmp/ptouch-final-image-smoke2.log`. +The durable conventional CUPS/Web fixture passes too. No extra physical labels +were printed during these software checks. + +Tasks 3.7 and 7.3 require a separately authorized integrated physical acceptance +and uncertain-output recovery session; the completed eight-label approval does +not authorize further printing. Task 7.5 remains pending until those requirements +are fulfilled and the change can be synced/archived. No image has been deployed, +no commits/branches pushed, and no PR opened. Strict OpenSpec validation currently +passes; this is not archival or a waiver of the physical checks. + +The remaining physical session is concretely bounded in +`tests/HARDWARE-ACCEPTANCE.md`: 20 integrated acceptance labels plus one recovery +label, with an incomplete upload and blocked attempt issuing no print/feed command. +Printed content is about 54 cm before unmeasured leaders/feed. This is prepared only, +not authorized or run; it adds no physical evidence to the completed eight-label +gate. Approval must precede printer-host setup or additional output. + +## Integrated physical session authorization + +The operator answered "12mm in, ready" to the proposed bounded 21-label session. +This authorizes the documented matrix and explicit recovery, starting with 12 mm +tape, not permanent deployment or unbounded repeats. The existing `vault` service +is running the original image `sha256:8f0de486c933c9eca20a772be9cd647bf377b53114c8d20cdb5805336d2a8ec0`; +its standard `jobs` CLI reports no jobs, and `status` reports idle. Evidence will +be retained at `vault:/tmp/ptouch-integrated-acceptance.Nj73Wz/`. +No labels have yet been sent in this session. + +### Integrated stage 1: three chain-only PNG copies on 12 mm + +[verified] The candidate preflight reports clean 12 mm tape, canonical 63/71 margins, +and no jobs. One normal IPP PNG job with three copies completed successfully. +Transparent test-only USB recording contains 180/180/180 raster rows, mandatory +first precut and between-label autocut, one initialization, three FF commands, +zero EJECT, and clean completion/receiving-ready after each label. Three of 21 +complete labels have been sent. Physical separation, leader, and held-end +observations are pending. The original service remains temporarily stopped and +the isolated candidate remains running to preserve the healthy held run. +Evidence: `vault:/tmp/ptouch-integrated-acceptance.Nj73Wz/stage 1.log`, +`usb-io.jsonl`, and local `/tmp/ptouch-integrated-first/stage 1.print.prn`. + +The operator confirms stage 1: "leader and 2 labels with rectangles cut off, last +one held". This matches the mandatory fresh-run precut, two between-label cuts, +and the held final label. Stage1 passes physically; three of 21 labels are complete. +The candidate remains running across this idle gap. All remaining 12 mm fixtures +were independently verified through isolated IPP/socket captures before hardware +use, including exact PNG/PWG pixels and incomplete input after 60 rows with no +FF/EJECT and a blocked following job. No additional physical labels were sent. + +### Integrated offline queue check + +[verified] After the operator disconnected USB, job 2 (two chain-only PNG copies) +was accepted and waits for the device with zero impressions. PAPPL reports this +active waiting job as processing, so the fixture's initial pending-only assertion +was corrected using a read-only query; the job was not replayed. Job 3 (one +chain-and-final PNG) was accepted held for the next inspection. Captured USB +output remains exactly unchanged at 10326 bytes. Two valid jobs are queued while +the printer is disconnected; physical resumption is pending. No labels beyond +the first three have been printed. Evidence: `stage 2-queue.log`, +`stage 2-offline-check.log`, and `stage 3-queue.log` in the session directory. + +### Integrated stages 2/3: offline continuation and finishing + +[verified] After USB reconnection, the queued two-label chain job completed with +180/180 rows, two FF commands, zero EJECT and no initialization. The operator +reports the prior held label cut free, then one new label cut free and the second +held, with no new blank leader. Releasing the existing held one-label finishing +job then produced 180 rows, late chain-off and EJECT, again without initialization. +The operator confirms the prior label was released and the new label cut free. +Both queued jobs resumed without replay or a discarded leader. The idle gap +between stage 1 and stage 2 was about an hour. Six of 21 labels are complete and +the run is physically confirmed closed. Captures and job completions are retained +in `stage 2-completed.log`, `stage 3-completed.log`, and `usb-out.bin`. + +The operator confirms stage 3: prior held label released, one new label printed +and cut free. Both physically inspected offline/continued jobs pass. A concrete +24 mm media-col request while 12 mm is loaded then aborted with "Page geometry does +not match loaded 12 mm tape". Its capture contains only two status requests and +no initialization, raster, FF or EJECT. The fixture was not replayed. The candidate +remains healthy after the rejection; the next counted stage is the two-label +no-cut batch. Evidence: `wrong-tape.log` and its captured output boundary. + +### Integrated stage 4: two no-cut PNG copies + +[verified] The two-copy enum-none job completed with 180/180 rows, first-label +precut enabled and later autocut disabled, two FF commands, zero EJECT and one +fresh initialization after the preceding confirmed final cut. Eight of 21 +complete labels have been sent. Physical joined-strip/leader observations are +pending. The candidate stays running with its final strip held. +Evidence: `stage 4.log`, `stage 4-offset`, and `usb-out.bin`. + +The operator confirms stage 4: "leader cut off, then 2 labels, both held". +The joined two-label strip remains attached, matching no internal or terminal +cut. Stage 4 passes physically. The following no-cut batch is submitted as a +healthy continuation, with no server restart or reset. + +### Integrated stage 5: second no-cut batch + +[verified] The second two-copy enum-none job completed with 180/180 rows, first +precut enabled, later autocut disabled, two FF commands, zero EJECT, and no +initialization. Ten of 21 complete labels have been sent. The operator reports +"leader cut off, next 2 labels held"; clarification is pending whether "leader" +means the previous joined two-label strip, rather than new blank scrap. +The healthy run remains held; no following batch has yet been submitted. +Evidence: `stage 5.log`, `stage 5-offset`, and `usb-out.bin`. + +The operator clarifies stage 5's "leader" was the previous 2-label strip. +This confirms separate joined strips across consecutive enum-none batches, +mandatory batch-boundary precut, and no discarded blank leader. Stage 5 passes +physically, with ten of 21 labels complete. The one-label final-only continuation +is now submitted to release the held strip and close the run. + +### Integrated stage 6: final-only continuation + +[verified] The one-label enum 63 job completed with 180 rows, late chain-off/EJECT, +mandatory first precut, and no initialization or empty terminal command. The +operator reports the prior two-label strip released and the new label printed +and cut free. The none/none/final-only sequence passes physically without a new +blank leader between healthy batches. Eleven of 21 complete labels are sent; +the run is confirmed closed. Evidence: `stage6.log` and its command capture. + +### Integrated stage 7: three chain-and-final PNG copies + +[verified] Three-copy enum 60 PNG job completed with 180/180/180 rows, mandatory +initial precut and internal cutting, FF/FF/late-chain-off-EJECT, one fresh +initialization, and no empty terminal command. The operator confirms one leader +and three labels cut free. Fourteen of 21 complete labels are sent; the run is +confirmed closed. Evidence: `stage7.log` and the stage 7 command boundary. + +### Integrated stage 8: unknown-count three-label raster + +[verified] One standard PWG job with optional total-page count 0 completed with +180/216/180 rows, FF/FF/late-chain-off-EJECT, one fresh initialization, and no +empty terminal command. The operator confirms three labels, all cut free. +Seventeen of 21 complete labels are sent; the run is confirmed closed. Physical +length/orientation marker details still require inspection. Evidence: +`stage8.log`, its output boundary, and the validated `raster-3.pwg` fixture. + +### Integrated stage 9: three-copy final-only strip + +[verified] The landscape PNG job completed three 180-row labels, FF/FF/late-chain-off +EJECT, first autocut enabled and later cuts disabled, one fresh initialization, +and no empty terminal command. The operator confirms one leader and a joined +three-label strip cut free at the end. Twenty of 21 complete labels are sent; +the run is confirmed closed. Evidence: `stage9.log` and its capture boundary. + +The stage 8 raster test client timed out after 10 seconds because streaming +Print-Job responds after physical printing. No job was replayed. A read-only +Get-Job-Attributes for its existing job 9 confirms completed state 9 and three +impressions; server logs and the 576-row capture agree. Subsequent physical +streaming checks use a 45-second client timeout. This is a test-timeout correction, +not a relaxed completion/status requirement. Evidence: `stage8-confirm.log`. + +### Integrated uncertain-output blocking + +[verified] The authorized incomplete single-page PWG upload aborted after 60 raster +rows with no FF/EJECT, so no complete label was printed. A status-only probe on a +fresh USB connection reports clean 12 mm tape and error 0000/phase 00. The following +valid marked PNG still aborts with "Output uncertain: power-cycle the printer, +then restart the server before resubmitting", and its capture has zero setup, +raster, feed or cut. Clean status and USB reopen did not clear the latch. Twenty +of 21 complete labels remain printed; explicit power-cycle/restart and the single +marked recovery label are pending. Evidence: `incomplete.log`, +`status-while-uncertain.log`, `blocked.log`, and corresponding capture boundaries. + +The operator replies "ready" after the explicit printer power-cycle instruction. +Only the isolated candidate server is being restarted; failed jobs are not +resubmitted automatically. The final marked recovery label is submitted explicitly +after clean 12 mm/no-jobs preflight. Physical absence of ghost output and final +marker/geometry inspection remain pending operator confirmation. + +### Integrated stage 10: explicit recovery label + +[verified] After the operator's power-cycle and the candidate-only restart, +preflight reports idle, zero jobs, clean 12 mm tape and canonical margins. The +explicitly resubmitted marked recovery label completes with 180 rows, one fresh +initialization and one late-chain-off EJECT. No additional raster image or print +command is emitted. All 21 authorized complete labels have now been sent; the +incomplete/blocked trials emitted no FF/EJECT and no complete label. Physical +absence of ghost output and final marker inspection are pending confirmation. +The original deployed image/configuration remains unchanged; its service is +still temporarily stopped until recovery/closed-run inspection is confirmed. +Evidence: `recovery-preflight.log`, `stage10.log`, and the final capture boundary. + +The operator confirms the recovery output: "leader and one label cut off". +No additional fragment or replay is reported, matching the single 180-row capture +and completed recovery job. Physical power-cycle followed by server restart and +explicit resubmission passes. README recovery/job-inspection guidance and +ADR-0002/0007 were reviewed; README now supplies the concrete jobs/logs/status +commands and distinguishes failed-job inspection from queue persistence. + +[verified] The temporary candidate was stopped and removed after this physically +confirmed final cut. The original `ptouch` service is restored, running, and idle +on the unchanged image `sha256:8f0de486c933c9eca20a772be9cd647bf377b53114c8d20cdb5805336d2a8ec0`. +Its queue has no jobs. All 21 complete labels and the bounded negative trials are +finished. Final physical geometry/orientation marker confirmation remains open +before task 7.3 and archival can close. No further printing is authorized. + +The operator's final layout confirmation is "all good": complete rectangle +outlines, the longer middle raster label, and upright text on the joined strip. +All 21 physical labels and the bounded negative/recovery trials pass on 12 mm tape. +Combined with the earlier approved eight-label 24 mm protocol gate and five-width +software captures, this satisfies the recorded physical acceptance scope; no +hardware claims are made for 6/9/18 mm tape. The original service is restored. +The user subsequently requested Claude review of the implementation before final +sync/archive; no additional printing, publishing or deployment is authorized. + + +## Final implementation review fixes, 2026-10-13 + +[verified] Claude identified named-only media geometry, busy-status publication, +and stale documentation. Its complete review is retained under evidence/. +Named-only collection media now preserves a PNG's 50 mm page and rejects conflicting +raster dimensions before page output, matching PAPPL's size-name precedence. +The new raster reproduction failed on the previous SDK and passes 47 IPP jobs plus +three negative-I/O cases on the corrected one. Patch application passes with +fuzz 0, the finishing patch still applies afterward, and duplicate/corrupt-context +applications fail. The production PNG fixture proves 354 rows for 50 mm and zero +output for too-short or wrong-tape named media. + +[verified] Idle status no longer publishes recovery/offline during active printing; +the NULL-open branch rechecks processing to distinguish scheduling from claim +failure. A real scheduled-job regression preserves media confirmation and reasons +without extra status reads. Existing idle unclaimable-USB rejection still passes. +The focused driver and PNG suites pass 2/2 in 29.51 s under Release with no warnings. + +[verified] The missing ordinary CUPS cases are now included in scripts/check-cups.py. +Two copies of the two-label PDF produce four 354-row labels. CUPS's Custom.12x40mm +submits 1199 × 3986 hundredths of a millimeter and explicit 297/297 margins; it takes the maximum margins +across tapes and flattens per-width custom ranges into one PPD custom size. +The server now advertises only ±0.01 mm width tolerance around each nominal tape, +retains submitted dimensions and explicit margins, and keeps the physical window +unchanged. All five widths accept ±0.01 mm and reject ±0.02 mm with fidelity enabled. A fitting +42-dot custom PNG produces 282 rows; the original 76-dot PNG fails with the correct +42-dot limit and zero output. Exact standard media-col 1200 × 4000 with the actual +63/71 tape margins prints the 76-dot design in 283 rows. No hidden margin replacement, +resizing, or private client protocol was added. This documents the remaining +CUPS PPD limitation rather than claiming full-width arbitrary Custom designs work. + +[verified] The conventional queue also passes PNG/JPEG, registered 60/63, offline +queue/resume, chain collection, and existing Web-default preservation cases. +The final runtime build passes all 11 CTest suites in 120.50 s. Its isolated image +smoke passes four presets, synchronized defaults, explicit geometry, copies, +and malformed-input failure. Read-only focused review finds no actionable defect +in the fixes. The redundant CI target-build invocation was removed; runtime +building still requires the complete test stage before smoke and publication. +The test-only batch probe is retained as the reproducible protocol-gate fixture; +it is not a second production lifecycle or public protocol. + +[verified] Current documentation records both passed physical sessions and keeps +historical checkpoints explicitly marked as history. Raw physical evidence, +operator observations, regressions, CUPS integration, full build, and smoke +results are retained under evidence/ with a SHA-256 manifest. No further labels +were printed, no commits or branches were pushed, and production was not changed. +Only specification sync, strict validation, and local archival remain. + + +## Local archival, 2026-10-13 + +[verified] The batch-printing, label-rendering, and printer-device specifications +were synced and compared against every delta requirement. Existing batch +purpose and scenario names were preserved. Strict validation passed all three +specifications and the active change before archival. All 42 tasks were checked +and verified through the CLI; archival completed locally with specs already +synced. Publishing and production deployment remain separate authorized actions. diff --git a/openspec/specs/batch-printing/spec.md b/openspec/specs/batch-printing/spec.md index ca5e2ea..97407a4 100644 --- a/openspec/specs/batch-printing/spec.md +++ b/openspec/specs/batch-printing/spec.md @@ -8,64 +8,156 @@ Let clients choose cutting for each label job through the printer's advertised f ### Requirement: Job finishing controls cutting -The server SHALL use the job's resolved finishing option as its cut policy. `none` SHALL disable all cuts and leave the tape run open. `trim` SHALL cut the job's labels individually and finalize its last label. Queue contents and legacy cut-mode environment settings SHALL NOT override this choice. +The server SHALL support four presets: cuts between labels with the batch left open; cuts between labels plus a final batch cut; no label cuts with the batch left open; and only a final batch cut. Mandatory precut is separate batch preparation. `none` selects no label cuts and continuation; `trim` aliases cuts between labels plus the final cut. Queue contents, idle time, and legacy environment settings SHALL NOT choose or override finishing. #### Scenario: Explicit no-cut job - **WHEN** a valid job requests finishings none -- **THEN** it prints without precut, intermediate cuts, or terminal cutting, even if the queue becomes empty +- **THEN** mandatory precut prepares the first label, and the batch prints without cuts between its labels or a final cut, even if the queue becomes empty #### Scenario: Explicit trim job - **WHEN** a valid job requests finishings trim -- **THEN** it cuts and finalizes its labels, even if another job is queued +- **THEN** its labels are cut apart using one chained run, and only the batch's last label receives final feed/cut, even if another job is queued #### Scenario: Finishings collection -- **WHEN** a job selects a supported none or trim finishing-template through finishings-col -- **THEN** it behaves like the corresponding finishings enum selection +- **WHEN** a job selects a supported finishing template through finishings-col +- **THEN** it uses that template's advertised preset, and registered templates behave like their corresponding finishings enums + +#### Scenario: Chain cuts without final cut +- **WHEN** a three-label batch requests the chain-only preset +- **THEN** its first two labels are separated and its last label remains attached without terminal feed/cut + +#### Scenario: Final cut only +- **WHEN** a three-label batch requests trim-after-job +- **THEN** mandatory precut occurs once, the three labels remain joined, and one final cut releases the strip ### Requirement: Advertised defaults are honored -A job that omits its finishing selection SHALL use the printer's advertised finishing default. The server SHALL NOT substitute a hidden environment or queue-based cut policy. +The server SHALL initialize finishings-default and finishings-col-default consistently to trim. An omitted job selection SHALL use one canonical default, updated through standard management of either representation. Defaults SHALL support the three presets with enum equivalents, with collection no-value for enum none. Chain-only and contradictory default updates SHALL be rejected without changing defaults. Explicit job selections SHALL override the default without hidden environment or queue-based policy. + +#### Scenario: No finishing option +- **WHEN** the client omits finishings and the advertised default is trim +- **THEN** its labels are cut apart and the batch finishes using the advertised trim default #### Scenario: No finishing option with none default -- **WHEN** the client omits finishings and the advertised default is none -- **THEN** the job prints without cutting +- **WHEN** the client omits finishings and the current advertised default has been changed to none +- **THEN** mandatory precut occurs, followed by no inter-label or final batch cuts, honoring the advertised default + +#### Scenario: Explicit none overrides default +- **WHEN** the client explicitly requests none +- **THEN** mandatory precut remains, but no inter-label or final batch cuts are introduced by the trim default + +#### Scenario: Consistent default updates +- **WHEN** an operator sets the collection default to trim-after-job and later sets the enum default to none +- **THEN** each update republishes consistent enum/collection defaults, omitted jobs use the latest preset, and none has a no-value collection default + +#### Scenario: Chain-only is an explicit job choice +- **WHEN** an operator attempts to set ptouch-chain as the printer default +- **THEN** the update is rejected without changing the existing defaults, while an explicit ptouch-chain job remains supported ### Requirement: Explicit continuation survives idle time -On a healthy matching-tape printer, the server SHALL preserve an unfinished run across subsequent jobs without resetting it or cutting due to elapsed idle time. A later trim job SHALL finalize its own final label using its explicit policy. No label content SHALL be resent during cleanup. +On a healthy matching-tape printer, the server SHALL preserve a deliberately unfinished run across idle time and subsequent batches. A later batch's mandatory precut SHALL separate the preceding batch's held end without generating another discarded leader. Its own finishing preset SHALL determine its internal and final cuts. Cleanup SHALL NOT resend label content. #### Scenario: Idle gap between jobs -- **WHEN** a no-cut job completes and another valid job arrives after an idle gap -- **THEN** the open run is preserved without an automatic cut or printer reset between jobs +- **WHEN** an unfinished batch completes and another valid batch arrives after an idle gap +- **THEN** the open run is preserved without an idle cut or printer reset, and the next batch's precut separates the held end without discarded leader waste #### Scenario: Terminal trim job -- **WHEN** one or more no-cut jobs are followed by a trim job -- **THEN** the server continues the existing run and finishes the trim job with a terminal cut without an extra blank label +- **WHEN** one or more unfinished batches are followed by a trim job +- **THEN** the server continues the same tape run, applies the new batch's precut and internal cuts, and finishes its last label without an extra blank label #### Scenario: Missing terminal job -- **WHEN** a terminal job is never submitted, canceled, or rejected before printing -- **THEN** idle cleanup does not cut the preceding no-cut run +- **WHEN** a terminal batch is never submitted, canceled, or rejected before printing +- **THEN** idle cleanup does not cut or reset the preceding healthy unfinished run + +#### Scenario: Consecutive batches without internal cuts +- **WHEN** two batches request none +- **THEN** labels remain joined within each batch, while the second batch's precut separates the first batch's strip without generating a discarded leader ### Requirement: Failure does not override the finishing choice -A detected output or printer-status failure SHALL fail the job without replaying its content or inventing a terminal cut. Successful finalization SHALL only be recorded after the requested terminal command is transferred. If continuation state cannot be retained, a no-cut job SHALL fail before printing rather than cut implicitly. +A detected output, raster decode, or printer-status failure SHALL fail the job without replaying content or inventing a final cut. Finalization SHALL succeed only after its requested terminal command is transferred and readiness confirmed. Ambiguous partial output SHALL block automatic continuation pending explicit recovery. Rejected or canceled jobs that sent no output SHALL preserve a prior healthy run. #### Scenario: No-cut job canceled during printing -- **WHEN** a no-cut job is canceled after printing starts -- **THEN** cleanup does not introduce a terminal cut +- **WHEN** a no-label-cuts job is canceled after printing starts +- **THEN** cleanup introduces no final cut and uncertain buffered output is not silently reused by later work #### Scenario: Terminal transfer fails -- **WHEN** the requested terminal cut cannot be transferred -- **THEN** the job reports failure and repeated cleanup does not resend the terminal command or label content +- **WHEN** the requested final cut cannot be confirmed +- **THEN** the job reports failure and repeated cleanup does not resend terminal commands or label content + +#### Scenario: Rejected follower +- **WHEN** a subsequent batch is rejected before sending any page setup or raster +- **THEN** it neither precuts nor damages the prior healthy unfinished run + +#### Scenario: Truncated final label +- **WHEN** a streamed batch ends with an incomplete raster label +- **THEN** the job aborts, the incomplete label receives no normal print/feed or final-cut command, and uncertain output requires explicit recovery ### Requirement: Existing rendering is preserved -The server SHALL retain advertised document formats, 1:1 PNG geometry, strict tape-width rejection, supported copy limits, and offline media recovery. The finishing choice SHALL NOT change rendered pixels or dimensions. Physical printing and cutting SHALL be verified on the PT-2730 before claiming the leader issue fixed. +The server SHALL retain 1:1 default printing, strict tape-width rejection, and offline media recovery as defined by label-rendering and printer-device. The finishing choice SHALL NOT change rendered pixels or page dimensions. Advertised formats and copy limits SHALL describe executable behavior. Changed physical printing and cutting SHALL be verified on the PT-2730 before claiming hardware success. #### Scenario: Copies -- **WHEN** a valid PNG requests supported copies -- **THEN** all requested copies use the same finishing policy and existing image geometry +- **WHEN** a valid PNG requests copies within its advertised supported range +- **THEN** all requested copies use the resolved finishing policy and the same image geometry #### Scenario: Offline printer returns - **WHEN** queued jobs resume after a printer returns with matching tape -- **THEN** media is confirmed through the existing recovery path and each job retains its requested finishing choice +- **THEN** media is confirmed before printing and each job retains its requested finishing choice + +### Requirement: Actual pages determine label boundaries + +One IPP job SHALL define one batch. PNG copies and actual PWG raster or URF labels SHALL receive the selected batch policy without treating copies as document pages. Successful end of input SHALL determine the final label even when the total count is omitted. The server SHALL distinguish normal completion from abort cleanup and never infer batches from queued jobs or idle time. + +#### Scenario: Two-page raster with one copy +- **WHEN** a valid two-page PWG raster job requests one copy and trim +- **THEN** both labels print in one chain, their shared boundary is cut, the final label is cut free, and the job completes successfully + +#### Scenario: Unknown page count +- **WHEN** a valid streaming raster job omits its total page count +- **THEN** every complete label uses the requested policy, with the final feed/cut decided at successful end of the batch, without requiring custom count metadata + +#### Scenario: No-cut multi-page job +- **WHEN** a valid multi-page job explicitly requests none +- **THEN** precut prepares only the first label, the labels remain joined, and the batch stays open for explicit continuation + +### Requirement: Precut is mandatory batch preparation + +The server SHALL prepare the first label of every nonempty batch with precut, including no-label-cuts and final-only batches. Precut SHALL NOT enable cuts between later labels when that preset forbids them. A healthy continuation SHALL use the existing tape boundary without producing discarded leaders between batches. A new closed run can require one initial leader. + +#### Scenario: Fresh batch +- **WHEN** a nonempty batch begins on a closed run with any preset +- **THEN** its initial leader is cut off before the first label + +#### Scenario: Continuing batch +- **WHEN** a nonempty batch follows a healthy unfinished batch +- **THEN** precut separates the prior held end without creating a discarded inter-batch leader + +#### Scenario: Mandatory precut does not imply cut-each +- **WHEN** a three-copy PNG requests none or trim-after-job +- **THEN** precut applies to the first copy only and no cuts are introduced between the copies + +### Requirement: Finishing presets use truthful IPP meanings + +The server SHALL advertise none (3), trim (11), trim-after-pages (60), and trim-after-job (63). `trim` and 60 select cuts between labels plus the final cut; 63 selects only the final cut. Non-none enums SHALL have matching templates; collection no-value SHALL select none. Chain-only SHALL use the advertised implementation-defined `ptouch-chain` template through standard finishings-col. No private job attribute SHALL supply an independent final-cut switch. + +#### Scenario: Registered selections +- **WHEN** a client selects none by enum or collection no-value, or trim-after-pages or trim-after-job by enum or template +- **THEN** it receives that advertised policy without silently collapsing the selection to none or trim + +#### Scenario: Chain-only selection +- **WHEN** a client selects finishing-template ptouch-chain through finishings-col +- **THEN** cuts occur between labels while the final label remains attached + +#### Scenario: Redundant registered combination +- **WHEN** a client requests finishings 60 and 63 together +- **THEN** all labels are cut apart with one final batch cut, without a duplicate terminal cut or a claim that these values are independent switches + +#### Scenario: None alongside another finishing +- **WHEN** a client supplies enum none alongside supported finishing values +- **THEN** none is ignored as required by IPP and the remaining values determine the selected preset + +#### Scenario: Conflicting or unsupported selection +- **WHEN** a client requests an unsupported preset, a conflicting preset combination, or both finishings and finishings-col +- **THEN** the request is rejected with an appropriate standard IPP error before any print output diff --git a/openspec/specs/label-rendering/spec.md b/openspec/specs/label-rendering/spec.md new file mode 100644 index 0000000..4bc401d --- /dev/null +++ b/openspec/specs/label-rendering/spec.md @@ -0,0 +1,95 @@ +# label-rendering Specification + +## Purpose + +Let ordinary CUPS and IPP clients print labels using standard document formats, media dimensions, and truthful capabilities without silent resizing or content loss. + +## Requirements + +### Requirement: Explicit media geometry is authoritative + +For image jobs, the server SHALL honor an explicitly selected IPP media size, media-col.media-size-name, or media-col dimensions. PNG auto-length SHALL apply only when the submitted attributes omit a concrete media length. A source-only or margin-only media-col SHALL NOT disable that fallback. + +#### Scenario: Explicit PNG length +- **WHEN** a 180-pixel-long PNG at 180 dpi requests 50 mm media length +- **THEN** the printed page remains 50 mm long rather than being replaced with 25.4 mm + +#### Scenario: PNG without length +- **WHEN** a valid PNG omits concrete media length +- **THEN** its length follows the along-tape pixel dimension and DPI, within the existing 5 to 300 mm limits + +#### Scenario: Collection without dimensions +- **WHEN** a valid PNG supplies media-col with only a source or margins +- **THEN** image-derived length remains available + +### Requirement: Raster pages retain their submitted geometry + +The server SHALL use each PWG raster or URF page's submitted dimensions and resolution, validating them against supported media and the loaded tape. It SHALL NOT replace those dimensions with the default page, or silently pad or discard raster lines. Contradictory explicit job and page geometry SHALL fail visibly. + +#### Scenario: Short raster page +- **WHEN** a valid raster page is 25.4 mm long and omits explicit IPP length +- **THEN** exactly that page length prints without padding to the 100 mm default + +#### Scenario: Bit-depth independence +- **WHEN** equivalent supported 1-bit and 8-bit raster pages are submitted +- **THEN** conversion preserves their dimensions and content placement + +### Requirement: Incomplete raster input fails visibly + +The server SHALL reject short raster rows, incomplete declared page data, and malformed stream termination rather than completing the job with missing content. It SHALL withhold the affected label's normal print/feed or final-cut command, report job failure, and require recovery when already-transferred output is uncertain. Already printed labels SHALL NOT be replayed. + +#### Scenario: Partial final raster label +- **WHEN** a raster stream stops before supplying all of its final label's declared rows +- **THEN** the job aborts and the partial label receives no normal finishing command + +#### Scenario: Short row or malformed next header +- **WHEN** a raster row is incomplete or the next header is malformed +- **THEN** decode failure is not interpreted as successful end of the batch + +### Requirement: Default 1:1 PNG rendering does not discard image content + +With print-scaling none, the server SHALL reject PNG content that cannot fit the imageable area before sending page raster. It SHALL accept an imageable-area PNG that fits, or a nominal-width PNG canvas whose non-imageable margins are blank. It SHALL honor explicit orientation and retain the documented DPI fallback for PNGs without resolution metadata. + +#### Scenario: Oversized source image +- **WHEN** a portrait 200 by 300 pixel PNG at 180 dpi is sent to 12 mm tape with print-scaling none +- **THEN** the job fails with a useful geometry error and no page raster is emitted + +#### Scenario: Nominal-width canvas +- **WHEN** a correctly sized nominal-width image contains only blank pixels outside the tape's imageable window +- **THEN** its printable content is retained at the requested physical size + +#### Scenario: Explicit page too short +- **WHEN** a PNG at 1:1 exceeds the explicitly requested page length +- **THEN** it is rejected rather than cropped + +### Requirement: Advertised tape media is usable + +The server SHALL advertise supported nominal tape widths and publish their actual imageable margins through media-ready and media-default after confirmation. Every advertised tape width SHALL be accepted by media publication and printing. Geometry validation SHALL use the physical tape's imageable window, rather than a different tape's cached margins. + +#### Scenario: All advertised widths +- **WHEN** each advertised 6, 9, 12, 18, or 24 mm tape is reported loaded +- **THEN** media publication succeeds with that width and its correct margins + +#### Scenario: Wrong tape +- **WHEN** a submitted page width differs from the loaded tape +- **THEN** the job fails before its raster reaches the printer + +#### Scenario: CUPS custom-size rounding +- **WHEN** CUPS rounds a custom nominal tape width by at most 0.01 mm +- **THEN** the advertised width range accepts the request while preserving its submitted geometry, explicit margins, and physical imageable window + +#### Scenario: Conservative CUPS custom margins +- **WHEN** a custom-size request contains larger explicit margins than the tape requires +- **THEN** fitting content prints at 1:1 and oversized content fails before raster, without replacing the margins or shrinking the image + +### Requirement: Capabilities describe executable inputs + +The server SHALL advertise only printable public document formats, with no private filter destination masquerading as an input. It SHALL honor its advertised format-specific copies range; PNG copies SHALL support 1 through 999, and streaming raster SHALL advertise one copy with multiple document pages supported. + +#### Scenario: Format query +- **WHEN** a client queries document-format-supported +- **THEN** supported standard formats remain available and image/vnd.ptouch-raster is absent + +#### Scenario: Former planner limit +- **WHEN** a valid PNG requests 65 copies +- **THEN** all 65 copies print without a planner-capacity failure diff --git a/openspec/specs/printer-device/spec.md b/openspec/specs/printer-device/spec.md new file mode 100644 index 0000000..fba2bdf --- /dev/null +++ b/openspec/specs/printer-device/spec.md @@ -0,0 +1,75 @@ +# printer-device Specification + +## Purpose + +Expose one stable IPP queue for verified hardware, select its USB device predictably, and report availability and faults accurately across printer power cycles. + +## Requirements + +### Requirement: Only supported hardware receives print output + +The server SHALL create operational queues only for implemented and verified model paths, initially PT-2730. Recognizing a USB ID SHALL NOT grant print support. Invalid or unsupported model configuration SHALL fail explicitly rather than fall back silently. + +#### Scenario: Unsupported Brother attached +- **WHEN** discovery finds a model without an implemented print path +- **THEN** no PT-2730 raster is sent to that model and the unsupported device is identified in diagnostics + +#### Scenario: Invalid configured model +- **WHEN** the configured model is unknown or unsupported +- **THEN** startup fails with a useful configuration error + +### Requirement: Device selection is unambiguous + +The server SHALL expose one application queue and consistently select its configured supported model. Multiple matching physical devices SHALL be reported as ambiguous; discovery or opening SHALL NOT silently choose the first match. USB renumbering of a single matching device SHALL NOT require queue replacement. + +#### Scenario: Two matching devices +- **WHEN** two PT-2730 units match the configured queue +- **THEN** printing is blocked with an ambiguity diagnostic + +#### Scenario: Device replug +- **WHEN** the sole matching printer is unplugged and reattached with another USB node number +- **THEN** the existing queue recovers without selecting a different model or requiring a server restart + +### Requirement: Availability and faults are reported truthfully + +The server SHALL retain the queue while hardware is absent and report standard IPP reasons for unavailability, missing tape, and printer faults. USB presence alone SHALL NOT establish readiness, and parsed printer error bits SHALL be checked before page raster output. Test transports SHALL NOT invoke USB operations on incompatible connection data. + +#### Scenario: Present device cannot open +- **WHEN** USB enumeration finds the printer but the server cannot claim it +- **THEN** the queue does not report the printer as ready merely because USB is present + +#### Scenario: Fault with recognized tape +- **WHEN** status reports a recognized tape and a printer error +- **THEN** that error is exposed and new page raster is withheld + +#### Scenario: Socket test transport +- **WHEN** a socket sink is configured while Brother USB hardware is present +- **THEN** status processing never passes the socket connection to USB-specific operations + +### Requirement: Offline recovery confirms media before printing + +The server SHALL accept queued work while the printer is off and resume valid work after it returns. Image page geometry derived from loaded media SHALL be constructed only after live media confirmation. Explicit raster page geometry SHALL be validated against the confirmed tape rather than silently changed to match it. + +#### Scenario: Start while off +- **WHEN** a PNG without explicit media geometry is queued while the server starts with the printer off, then the printer returns with supported tape +- **THEN** geometry uses that confirmed tape and the job can complete without a server restart + +#### Scenario: Existing unfinished run +- **WHEN** a healthy chain-only or no-label-cuts run remains open during status polling or a subsequent valid job +- **THEN** cassette detection continues without a printer reset that destroys the unfinished run + +### Requirement: Uncertain output requires explicit recovery + +When interrupted output leaves the printer's buffered label or final-cut result uncertain, the server SHALL report recovery required and block new raster output. It SHALL NOT blindly reset, print, cut, or append to that buffer. The documented conservative recovery SHALL power-cycle the printer and then restart the server. A clean status response or USB reopen alone SHALL NOT clear the uncertainty. + +#### Scenario: Interrupted label followed by another job +- **WHEN** raster transfer fails partway through a label and another job arrives +- **THEN** the next job emits no raster or automatic reset and the queue reports the required recovery action + +#### Scenario: Explicit recovery +- **WHEN** the operator power-cycles the printer and restarts the server after uncertain output +- **THEN** a new job confirms media and starts a fresh run without replaying the interrupted content + +#### Scenario: Rejected job before output +- **WHEN** validation rejects a job before page setup or raster is sent +- **THEN** a previously healthy unfinished run is not reclassified as uncertain merely because the job was rejected diff --git a/patches/README.md b/patches/README.md new file mode 100644 index 0000000..26a4200 --- /dev/null +++ b/patches/README.md @@ -0,0 +1,145 @@ +# Pinned dependency corrections + +Both image builds pin upstream PAPPL to **v1.4.11** and apply +`pappl-1.4.11-raster.patch` with `patch --batch --forward --fuzz=0 -p1` before compilation. +A changed dependency or a patch conflict must fail the image build. + +Upstream: https://github.com/michaelrsweet/pappl/tree/v1.4.11 +The modified upstream files retain their Apache-2.0 copyright/license headers. + +The raster correction addresses three demonstrated boundary defects: + +- Streamed geometry was copied only for input and output with at least eight + bits per pixel. A forced one-bit driver consequently received its default + media geometry. Preserve each submitted page while retaining the selected + one-bit conversion format and correctly sized buffers; reject contradictory + explicit media, including collection media-size-name, before the page + callback. Match PAPPL's resolved-name precedence when the collection also + supplies dimensions. Unsupported conversion must fail. +- A short input row was treated as an ordinary loop exit, after which a discard + loop hid the error and completed the page. Require each declared row in full, + with no default-page padding or discard path, and check every write callback. +- Image abort cleanup could call end-job while the job still appeared successful, + or call it twice after end-job failure. Publish aborted state before cleanup + and invoke that callback once. Stream cleanup likewise sees the failure state. + +This patch adds no raster decoder or page spooler. CUPS retains raster decoding, +PAPPL retains its existing dithering, and the driver owns physical finishing. +The separate finishing patch below handles advertising, acceptance, and +synchronized defaults. Neither patch implements Brother command policy. + +Remove these changes only when a pinned upstream release passes the same real +IPP/socket regression tests without the patch. A version upgrade alone is not +proof of equivalent behavior. + +## Bounded CUPS decoder correction + +Both images rebuild only `libcups` and `libcupsimage` from authenticated Debian +source version **2.4.2-3+deb12u9**, applying +`cups-2.4.2-deb12u9-raster.patch` through the shared +`scripts/build-cups.sh` helper. This retains Debian's full security patch set; +it does not replace the libraries with pristine upstream 2.4.2. The pinned +source must be refreshed when Debian publishes a replacement security revision, +with exact patch application and the raster regressions reverified. Build and +runtime guards reject a different Debian revision, preventing a cached pinned +library from silently replacing newer security fixes. The runtime records its +source version in `/usr/local/share/ptouch/cups-source-version`. + +Debian source: https://sources.debian.org/src/cups/2.4.2-3%2Bdeb12u9/ +Upstream decoder: https://github.com/OpenPrinting/cups/blob/v2.4.2/cups/raster-stream.c +The source retains its Apache-2.0 license and copyright headers. + +CUPS returned zero for both clean end-of-input and a truncated or invalid next +page header. Its compressed-row decoder can read ahead into a private buffer, +so HTTP byte counting cannot distinguish those cases outside CUPS. A small +header-read helper probes **one byte per header**, then reads the remainder +normally. An absent first byte remains normal EOF; a missing remainder records +an error through the existing `cupsRasterErrorString` API. Existing header +validation failures now record an error too, without changing their acceptance +criteria. Negative input callbacks also record failure, preserving zero-byte EOF +as the normal completion case. PAPPL checks that error before its end-job cleanup. + +No public API, ABI, raster parser, page spooler, scheduler, or finishing engine +is added. The helper preserves GnuTLS, Kerberos, Avahi, and Debian hardening +configuration. Only the ABI-compatible libraries are installed under +`/usr/local/lib`; Debian headers and command-line tools remain in use. The deploy +stage copies both patched libraries with PAPPL and runs `ldconfig`, so PNG/JPEG +and streamed raster decoding use the same patched runtime dependency. + +Remove the CUPS patch only when the pinned dependency distinguishes clean EOF, +partial headers, and invalid headers through the existing error API and passes +all regressions without it. Keep the PAPPL error propagation until the pinned +PAPPL release supplies equivalent verified behavior. + +## Bounded PAPPL finishing and media bridge + +Both image builds also apply `pappl-1.4.11-finishings.patch` after the raster +patch, with the same strict patch flags. PAPPL's legacy finishing bitmask cannot +distinguish trim-after-pages (60), trim-after-job (63), and a registered collection +template. Its generated attributes also duplicate driver declarations and use +one printer-wide margin for every roll size. A conventional CUPS queue therefore +submitted 12 mm media with 53/53 margins, instead of its physical 63/71 margins, +and correctly triggered the driver's printable-window rejection. + +The bridge activates only for drivers declaring both standard trim enums and +`finishings-col-database`. It replaces the explicit finishing declarations and +four canonical media attributes: `media-col-database`, `media-size-supported`, +`media-left-margin-supported`, and `media-right-margin-supported`. The driver +supplies each supported fixed tape width, variable length, and physical margins; +the bridge neither invents geometry nor relaxes raster validation. Legacy drivers +retain their existing generated attributes and finishing acceptance. + +Job validation checks raw standard enums or one advertised collection template, +rejects conflicting representations and unsupported values, and accepts collection +no-value for none. Raw job attributes remain available to the driver. Raster +copies are restricted to the already advertised one, while images retain up to +999 copies. The single printer default is kept synchronized across enum and +collection representations, IPP updates, the existing public default setter, and +PAPPL state save/load. A template without an enum equivalent cannot become the +canonical default. Invalid updates fail before changing defaults. The legacy Web +UI form preserves this default when saving unrelated fields, since its finishing +checkboxes cannot represent these presets. + +This is an internal bridge, with no public API/ABI change, new job attribute, +private finishing enum, command planner, or general finishing engine. Physical +cut policy and the implementation-defined `ptouch-chain` meaning belong to the +driver. Remove this patch only when a pinned upstream version passes the same +IPP/default/state and conventional CUPS regressions without it; a dependency +upgrade must revalidate both patches and their exact application. + +## Verification + +`test_pappl_raster` submits real IPP jobs to a minimal PAPPL driver and captures +converted raster at a TCP sink, with no USB access. It verifies 1-bit and 8-bit +PWG geometry/content, different page lengths with unknown total counts, 8-bit +URF, short rows, explicit-media contradictions, malformed/unsupported resolution, +unsupported RGB conversion, immediate write failure, and image failure cleanup. + +Malformed termination is part of the default CTest suite: one-byte and longer +partial next headers, invalid complete next headers, truncated first headers, +and truncated second-page compressed rows are tested for PWG and URF. Failed +jobs reach aborted cleanup without normal end-job finalization; already-completed +pages are not replayed. Valid unknown-count jobs following failures still work. +Direct CUPS I/O fixtures additionally fail the input callback after a complete +page for compressed PWG/URF and uncompressed CUPS raster. +The former optional failing `--partial-next-header` probe is now ordinary required +coverage. + +All patches use `--batch --forward --fuzz=0`: clean pinned application must +succeed; duplicate application and changed context must fail. Container build +and runtime library-resolution checks establish which libraries actually run; +command captures establish software behavior, not physical Brother certification. + +`test_pappl_finishings` verifies declared enums/templates, unambiguous attributes, +atomic and synchronized IPP/public defaults, state roundtrips, image/raster copy +limits, and legacy driver behavior. Production driver captures check the actual +four cut policies independently. The deploy-image smoke uses real IPP and a socket +sink; ordinary CUPS integration also verifies the canonical media metadata. These +software checks do not substitute for the separately recorded PT-2730 hardware +gates. + +The application initializes PAPPL's optional HTML footer to an empty string through +`papplSystemSetFooterHTML`. With v1.4.11 the default NULL footer crashes the existing +Printing Defaults renderer in localization; both the old and new app reproduce +this. The public-API initialization avoids extending the dependency patch and is +covered by the ordinary CUPS/Web UI fixture. diff --git a/patches/cups-2.4.2-deb12u9-raster.patch b/patches/cups-2.4.2-deb12u9-raster.patch new file mode 100644 index 0000000..eae4521 --- /dev/null +++ b/patches/cups-2.4.2-deb12u9-raster.patch @@ -0,0 +1,115 @@ +--- a/cups/raster-stream.c ++++ b/cups/raster-stream.c +@@ -68,6 +68,7 @@ + + static ssize_t cups_raster_io(cups_raster_t *r, unsigned char *buf, size_t bytes); + static ssize_t cups_raster_read(cups_raster_t *r, unsigned char *buf, size_t bytes); ++static ssize_t cups_raster_read_header(cups_raster_t *r, unsigned char *buf, size_t bytes); + static int cups_raster_update(cups_raster_t *r); + static ssize_t cups_raster_write(cups_raster_t *r, const unsigned char *pixels); + static void cups_swap(unsigned char *buf, size_t bytes); +@@ -615,7 +616,7 @@ + * Read it... + */ + +- if (cups_raster_read(r, (unsigned char *)&(r->header), len) < (ssize_t)len) ++ if (cups_raster_read_header(r, (unsigned char *)&(r->header), len) < (ssize_t)len) + { + DEBUG_printf(("4_cupsRasterReadHeader: EOF, r->iocount=" CUPS_LLFMT, CUPS_LLCAST r->iocount)); + return (0); +@@ -673,7 +674,7 @@ + 4 + }; + +- if (cups_raster_read(r, appleheader, sizeof(appleheader)) < (ssize_t)sizeof(appleheader)) ++ if (cups_raster_read_header(r, appleheader, sizeof(appleheader)) < (ssize_t)sizeof(appleheader)) + { + DEBUG_printf(("4_cupsRasterReadHeader: EOF, r->iocount=" CUPS_LLFMT, CUPS_LLCAST r->iocount)); + return (0); +@@ -723,7 +724,10 @@ + */ + + if (!cups_raster_update(r)) ++ { ++ _cupsRasterAddError("Invalid raster page header.\n"); + return (0); ++ } + + DEBUG_printf(("4_cupsRasterReadHeader: cupsColorSpace=%s", _cupsRasterColorSpaceString(r->header.cupsColorSpace))); + DEBUG_printf(("4_cupsRasterReadHeader: cupsBitsPerColor=%u", r->header.cupsBitsPerColor)); +@@ -733,7 +737,13 @@ + DEBUG_printf(("4_cupsRasterReadHeader: cupsHeight=%u", r->header.cupsHeight)); + DEBUG_printf(("4_cupsRasterReadHeader: r->bpp=%d", r->bpp)); + +- return (r->header.cupsBitsPerPixel > 0 && r->header.cupsBitsPerPixel <= 240 && r->header.cupsBitsPerColor > 0 && r->header.cupsBitsPerColor <= 16 && r->header.cupsBytesPerLine > 0 && r->header.cupsBytesPerLine <= 0x7fffffff && r->header.cupsHeight != 0 && (r->header.cupsBytesPerLine % r->bpp) == 0); ++ if (!(r->header.cupsBitsPerPixel > 0 && r->header.cupsBitsPerPixel <= 240 && r->header.cupsBitsPerColor > 0 && r->header.cupsBitsPerColor <= 16 && r->header.cupsBytesPerLine > 0 && r->header.cupsBytesPerLine <= 0x7fffffff && r->header.cupsHeight != 0 && (r->header.cupsBytesPerLine % r->bpp) == 0)) ++ { ++ _cupsRasterAddError("Invalid raster page header.\n"); ++ return (0); ++ } ++ ++ return (1); + } + + +@@ -1397,6 +1407,36 @@ + + + /* ++ * 'cups_raster_read_header()' - Distinguish clean EOF from a partial header. ++ * ++ * Probe one byte through the same decoder buffer before reading the remainder. ++ * cups_raster_read can return zero after consuming a partial buffered read, so ++ * its return value alone cannot establish that the next header was absent. ++ */ ++ ++static ssize_t ++cups_raster_read_header(cups_raster_t *r, unsigned char *buf, size_t bytes) ++{ ++ ssize_t count = cups_raster_read(r, buf, 1); ++ ++ if (count != 1) ++ { ++ if (count < 0) ++ _cupsRasterAddError("Unable to read raster stream."); ++ return (0); ++ } ++ ++ if (cups_raster_read(r, buf + 1, bytes - 1) != (ssize_t)(bytes - 1)) ++ { ++ _cupsRasterAddError("Truncated raster page header.\n"); ++ return (0); ++ } ++ ++ return ((ssize_t)bytes); ++} ++ ++ ++/* + * 'cups_raster_read()' - Read through the raster buffer. + */ + +@@ -1470,7 +1510,11 @@ + + remaining = (*r->iocb)(r->ctx, r->buffer, r->bufsize); + if (remaining <= 0) ++ { ++ if (remaining < 0) ++ _cupsRasterAddError("Unable to read raster stream."); + return (0); ++ } + + r->bufptr = r->buffer; + r->bufend = r->buffer + remaining; +@@ -1488,7 +1532,11 @@ + count = (*r->iocb)(r->ctx, buf, (size_t)count); + + if (count <= 0) ++ { ++ if (count < 0) ++ _cupsRasterAddError("Unable to read raster stream."); + return (0); ++ } + + #ifdef DEBUG + r->iostart += (size_t)count; diff --git a/patches/pappl-1.4.11-finishings.patch b/patches/pappl-1.4.11-finishings.patch new file mode 100644 index 0000000..e73719f --- /dev/null +++ b/patches/pappl-1.4.11-finishings.patch @@ -0,0 +1,520 @@ +--- a/pappl/printer-driver.c ++++ b/pappl/printer-driver.c +@@ -8,6 +8,7 @@ + // + + #include "printer-private.h" ++#include "finishings-private.h" + #include "system-private.h" + + +@@ -172,8 +173,17 @@ + pappl_pr_driver_data_t *data, // I - Driver data + ipp_t *attrs) // I - Additional capability attributes or `NULL` for none + { ++ int trim_default = -1; ++ bool trim_presets = _papplTrimPresets(attrs); + if (!printer || !data) + return (false); ++ if (trim_presets) ++ { ++ ipp_attribute_t *enums = ippFindAttribute(attrs, "finishings-default", IPP_TAG_ZERO); ++ ipp_attribute_t *col = ippFindAttribute(attrs, "finishings-col-default", IPP_TAG_ZERO); ++ trim_default = enums || col ? _papplTrimDefault(attrs, enums, col) : 3; ++ if (trim_default <= 0) return (false); ++ } + + // Validate data... + if (!validate_defaults(printer, data, data) || !validate_driver(printer, data) || !validate_ready(printer, data, data->num_source, data->media_ready)) +@@ -190,10 +200,33 @@ + printer->driver_attrs = make_attrs(printer->system, printer, &printer->driver_data); + + if (attrs) ++ { ++ if (trim_presets) ++ { ++ ipp_attribute_t *attr, *old; ++ const char *name; ++ // Explicit trim declarations and canonical roll geometry replace generated attributes. ++ for (attr = ippGetFirstAttribute(attrs); attr; attr = ippGetNextAttribute(attrs)) ++ { ++ name = ippGetName(attr); ++ if (name && (!strncmp(name, "finishings-", 10) || !strcmp(name, "finishing-template-supported") || ++ !strcmp(name, "media-col-database") || !strcmp(name, "media-size-supported") || ++ !strcmp(name, "media-left-margin-supported") || !strcmp(name, "media-right-margin-supported"))) ++ while ((old = ippFindAttribute(printer->driver_attrs, name, IPP_TAG_ZERO)) != NULL) ++ ippDeleteAttribute(printer->driver_attrs, old); ++ } ++ // Drop PAPPL's misspelled legacy collection names for this explicit path. ++ while ((old = ippFindAttribute(printer->driver_attrs, "finishing-col-database", IPP_TAG_ZERO)) != NULL) ippDeleteAttribute(printer->driver_attrs, old); ++ while ((old = ippFindAttribute(printer->driver_attrs, "finishing-col-supported", IPP_TAG_ZERO)) != NULL) ippDeleteAttribute(printer->driver_attrs, old); ++ } + ippCopyAttributes(printer->driver_attrs, attrs, 0, NULL, NULL); ++ } ++ ++ if (trim_presets) ++ _papplSetTrimDefault(printer->driver_attrs, trim_default); + + // Add finishings-col-default and finishings-default if missing... +- if (!ippFindAttribute(printer->driver_attrs, "finishings-col-default", IPP_TAG_BEGIN_COLLECTION)) ++ if (!trim_presets && !ippFindAttribute(printer->driver_attrs, "finishings-col-default", IPP_TAG_BEGIN_COLLECTION)) + { + ipp_t *col = ippNew(); // finishings-col-default value + +@@ -244,6 +277,19 @@ + return (false); + + _papplRWLockWrite(printer); ++ ++ // Prevalidate the existing public setter's finishing option atomically. ++ int trim_default = -1; ++ bool trim_presets = _papplTrimPresets(printer->driver_attrs); ++ if (trim_presets && (value = cupsGetOption("finishings", (cups_len_t)num_vendor, vendor)) != NULL) ++ { ++ trim_default = _papplTrimDefaultString(printer->driver_attrs, value); ++ if (trim_default <= 0) ++ { ++ _papplRWUnlock(printer); ++ return (false); ++ } ++ } + + // Copy xxx_default values... + printer->driver_data.bin_default = data->bin_default; +@@ -263,9 +309,14 @@ + printer->driver_data.x_default = data->x_default; + printer->driver_data.y_default = data->y_default; + ++ if (trim_default > 0) ++ _papplSetTrimDefault(printer->driver_attrs, trim_default); ++ + // Copy any vendor-specific xxx-default values... + for (i = 0; i < data->num_vendor; i ++) + { ++ if (trim_presets && !strcmp(data->vendor[i], "finishings")) ++ continue; + if ((value = cupsGetOption(data->vendor[i], (cups_len_t)num_vendor, vendor)) == NULL) + continue; + +--- a/pappl/printer-ipp.c ++++ b/pappl/printer-ipp.c +@@ -9,6 +9,7 @@ + // + + #include "pappl-private.h" ++#include "finishings-private.h" + + + // +@@ -989,6 +990,27 @@ + }; + + ++ int trim_default = -1; ++ bool trim_presets; ++ ipp_attribute_t *trim_enum = ippFindAttribute(client->request, "finishings-default", IPP_TAG_ZERO); ++ ipp_attribute_t *trim_col = ippFindAttribute(client->request, "finishings-col-default", IPP_TAG_ZERO); ++ ++ // Validate both default representations before changing either one. ++ _papplRWLockRead(printer); ++ trim_presets = _papplTrimPresets(printer->driver_attrs); ++ if (trim_presets && (trim_enum || trim_col)) ++ trim_default = _papplTrimDefault(printer->driver_attrs, trim_enum, trim_col); ++ _papplRWUnlock(printer); ++ if (trim_presets && (trim_enum || trim_col)) ++ { ++ if (trim_default <= 0) ++ { ++ if (trim_enum) papplClientRespondIPPUnsupported(client, trim_enum); ++ if (trim_col) papplClientRespondIPPUnsupported(client, trim_col); ++ return (false); ++ } ++ } ++ + // Preflight request attributes... + create_printer = ippGetOperation(client->request) == IPP_OP_CREATE_PRINTER; + +@@ -1017,6 +1039,9 @@ + papplClientRespondIPPUnsupported(client, rattr); + continue; + } ++ ++ if (trim_presets && (!strcmp(name, "finishings-default") || !strcmp(name, "finishings-col-default"))) ++ continue; // Already validated together above. + + // Validate syntax of provided attributes... + value_tag = ippGetValueTag(rattr); +@@ -1437,6 +1462,14 @@ + if (org_unit) + papplPrinterSetGeoLocation(printer, org_unit); + ++ if (trim_default > 0) ++ { ++ _papplRWLockWrite(printer); ++ _papplSetTrimDefault(printer->driver_attrs, trim_default); ++ printer->config_time = time(NULL); ++ _papplRWUnlock(printer); ++ _papplSystemConfigChanged(printer->system); ++ } + papplSystemAddEvent(printer->system, printer, NULL, PAPPL_EVENT_PRINTER_CONFIG_CHANGED, NULL); + + return (true); +@@ -2082,74 +2115,93 @@ + + if ((attr = ippFindAttribute(client->request, "copies", IPP_TAG_ZERO)) != NULL) + { +- if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_INTEGER || ippGetInteger(attr, 0) < 1 || ippGetInteger(attr, 0) > 999) ++ const char *format = ippGetString(ippFindAttribute(client->request, "document-format", IPP_TAG_MIMETYPE), 0, NULL); ++ if (ippGetCount(attr) != 1 || ippGetValueTag(attr) != IPP_TAG_INTEGER || ippGetInteger(attr, 0) < 1 || ippGetInteger(attr, 0) > 999 || ++ (format && (!strcmp(format, "image/pwg-raster") || !strcmp(format, "image/urf")) && ippGetInteger(attr, 0) != 1)) + { + papplClientRespondIPPUnsupported(client, attr); + valid = false; + } + } + +- if ((attr = ippFindAttribute(client->request, "finishings", IPP_TAG_ZERO)) != NULL) +- { +- if (ippGetValueTag(attr) != IPP_TAG_ENUM) +- { +- papplClientRespondIPPUnsupported(client, attr); ++ if (_papplTrimPresets(client->printer->driver_attrs)) ++ { ++ ipp_attribute_t *enums = ippFindAttribute(client->request, "finishings", IPP_TAG_ZERO); ++ ipp_attribute_t *col = ippFindAttribute(client->request, "finishings-col", IPP_TAG_ZERO); ++ if ((enums && col) || ++ (enums && (ippGetValueTag(enums) != IPP_TAG_ENUM || _papplTrimSelection(client->printer->driver_attrs, enums) < 0)) || ++ (col && ((ippGetValueTag(col) != IPP_TAG_BEGIN_COLLECTION && ippGetValueTag(col) != IPP_TAG_NOVALUE) || _papplTrimSelection(client->printer->driver_attrs, col) < 0))) ++ { ++ if (enums) papplClientRespondIPPUnsupported(client, enums); ++ if (col) papplClientRespondIPPUnsupported(client, col); + valid = false; + } +- else +- { +- for (i = 0, count = ippGetCount(attr); i < count; i ++) +- { +- int intvalue = ippGetInteger(attr, i); +- // finishings value +- pappl_finishings_t f; // finishings bit value +- +- if (intvalue == IPP_FINISHINGS_NONE) +- continue; +- +- f = _papplFinishingsValue(ippEnumString("finishings", intvalue)); +- if (!(client->printer->driver_data.finishings & f)) ++ } ++ else ++ { ++ if ((attr = ippFindAttribute(client->request, "finishings", IPP_TAG_ZERO)) != NULL) ++ { ++ if (ippGetValueTag(attr) != IPP_TAG_ENUM) ++ { ++ papplClientRespondIPPUnsupported(client, attr); ++ valid = false; ++ } ++ else ++ { ++ for (i = 0, count = ippGetCount(attr); i < count; i ++) + { +- papplClientRespondIPPUnsupported(client, attr); +- valid = false; +- break; ++ int intvalue = ippGetInteger(attr, i); ++ // finishings value ++ pappl_finishings_t f; // finishings bit value ++ ++ if (intvalue == IPP_FINISHINGS_NONE) ++ continue; ++ ++ f = _papplFinishingsValue(ippEnumString("finishings", intvalue)); ++ if (!(client->printer->driver_data.finishings & f)) ++ { ++ papplClientRespondIPPUnsupported(client, attr); ++ valid = false; ++ break; ++ } + } + } +- } +- +- if ((attr = ippFindAttribute(client->request, "finishings-col", IPP_TAG_ZERO)) != NULL) +- { +- papplClientRespondIPPUnsupported(client, attr); +- valid = false; +- } +- } +- else if ((attr = ippFindAttribute(client->request, "finishings-col", IPP_TAG_ZERO)) != NULL) +- { +- if (ippGetValueTag(attr) != IPP_TAG_BEGIN_COLLECTION) +- { +- papplClientRespondIPPUnsupported(client, attr); +- valid = false; +- } +- else +- { +- for (i = 0, count = ippGetCount(attr); i < count; i ++) +- { +- const char *template = ippGetString(ippFindAttribute(ippGetCollection(attr, i), "finishing-template", IPP_TAG_ZERO), 0, NULL); +- // finishing-template value +- pappl_finishings_t f; // finishings bit value +- +- if (template && !strcmp(template, "none")) +- continue; +- +- f = _papplFinishingsValue(template); +- if (!(client->printer->driver_data.finishings & f)) ++ ++ if ((attr = ippFindAttribute(client->request, "finishings-col", IPP_TAG_ZERO)) != NULL) ++ { ++ papplClientRespondIPPUnsupported(client, attr); ++ valid = false; ++ } ++ } ++ else if ((attr = ippFindAttribute(client->request, "finishings-col", IPP_TAG_ZERO)) != NULL) ++ { ++ if (ippGetValueTag(attr) != IPP_TAG_BEGIN_COLLECTION) ++ { ++ papplClientRespondIPPUnsupported(client, attr); ++ valid = false; ++ } ++ else ++ { ++ for (i = 0, count = ippGetCount(attr); i < count; i ++) + { +- papplClientRespondIPPUnsupported(client, attr); +- valid = false; +- break; ++ const char *template = ippGetString(ippFindAttribute(ippGetCollection(attr, i), "finishing-template", IPP_TAG_ZERO), 0, NULL); ++ // finishing-template value ++ pappl_finishings_t f; // finishings bit value ++ ++ if (template && !strcmp(template, "none")) ++ continue; ++ ++ f = _papplFinishingsValue(template); ++ if (!(client->printer->driver_data.finishings & f)) ++ { ++ papplClientRespondIPPUnsupported(client, attr); ++ valid = false; ++ break; ++ } + } + } + } ++ + } + + if ((attr = ippFindAttribute(client->request, "job-hold-until", IPP_TAG_ZERO)) != NULL) +--- a/pappl/job-process.c ++++ b/pappl/job-process.c +@@ -134,7 +134,7 @@ + options->finishings = PAPPL_FINISHINGS_NONE; + + if ((attr = ippFindAttribute(job->attrs, "finishings", IPP_TAG_ENUM)) == NULL) +- attr = ippFindAttribute(job->attrs, "finishings-col", IPP_TAG_BEGIN_COLLECTION); ++ attr = ippFindAttribute(job->attrs, "finishings-col", IPP_TAG_ZERO); + + if (!attr) + { +--- /dev/null ++++ b/pappl/finishings-private.h +@@ -0,0 +1,132 @@ ++// SPDX-License-Identifier: Apache-2.0 ++// Bounded bridge for drivers declaring standard trim presets plus templates. ++// No public API or printer command policy; the driver consumes raw job attrs. ++#ifndef PAPPL_FINISHINGS_PRIVATE_H ++#define PAPPL_FINISHINGS_PRIVATE_H ++ ++static inline bool ++_papplTrimPresets(ipp_t *attrs) ++{ ++ ipp_attribute_t *supported = ippFindAttribute(attrs, "finishings-supported", IPP_TAG_ENUM); ++ return (ippFindAttribute(attrs, "finishings-col-database", IPP_TAG_BEGIN_COLLECTION) && ++ ippContainsInteger(supported, 60) && ippContainsInteger(supported, 63)); ++} ++ ++static inline bool ++_papplHasFinishingTemplate(ipp_t *attrs, const char *name) ++{ ++ ipp_attribute_t *database = ippFindAttribute(attrs, "finishings-col-database", IPP_TAG_BEGIN_COLLECTION); ++ cups_len_t i; ++ if (!name) return (false); ++ for (i = 0; i < ippGetCount(database); i ++) ++ { ++ const char *value = ippGetString(ippFindAttribute(ippGetCollection(database, i), "finishing-template", IPP_TAG_KEYWORD), 0, NULL); ++ if (value && !strcmp(value, name)) return (true); ++ } ++ return (false); ++} ++ ++// -1 invalid, 0 an advertised template with no enum equivalent, else enum. ++static inline int ++_papplTrimSelection(ipp_t *attrs, ipp_attribute_t *selection) ++{ ++ ipp_attribute_t *supported = ippFindAttribute(attrs, "finishings-supported", IPP_TAG_ENUM); ++ int result = 3; ++ cups_len_t i; ++ if (!selection) return (-1); ++ if (ippGetValueTag(selection) == IPP_TAG_NOVALUE) ++ return (ippGetCount(selection) == 1 && ippContainsInteger(supported, 3) ? 3 : -1); ++ if (ippGetValueTag(selection) == IPP_TAG_ENUM && ippGetCount(selection) > 0) ++ { ++ for (i = 0; i < ippGetCount(selection); i ++) ++ { ++ int value = ippGetInteger(selection, i); ++ if (!ippContainsInteger(supported, value)) return (-1); ++ if (value == 3) continue; ++ if (value != 11 && value != 60 && value != 63) return (-1); ++ if (result == 3 || (result == 63 && (value == 11 || value == 60)) || value == 60) ++ result = value; ++ } ++ return (result); ++ } ++ if (ippGetValueTag(selection) == IPP_TAG_BEGIN_COLLECTION && ippGetCount(selection) == 1) ++ { ++ ipp_t *col = ippGetCollection(selection, 0); ++ ipp_attribute_t *member; ++ const char *name; ++ for (member = ippGetFirstAttribute(col); member; member = ippGetNextAttribute(col)) ++ if (!ippGetName(member) || strcmp(ippGetName(member), "finishing-template") || ippGetValueTag(member) != IPP_TAG_KEYWORD || ippGetCount(member) != 1) ++ return (-1); ++ name = ippGetString(ippFindAttribute(col, "finishing-template", IPP_TAG_KEYWORD), 0, NULL); ++ if (!_papplHasFinishingTemplate(attrs, name)) return (-1); ++ result = ippEnumValue("finishings", name); ++ return (result == 11 || result == 60 || result == 63 ? result : 0); ++ } ++ return (-1); ++} ++ ++// Existing vendor/state representation is a comma-separated enum list. ++static inline int ++_papplTrimDefaultString(ipp_t *attrs, const char *value) ++{ ++ int values[32], count = 0, result; ++ char *end; ++ ipp_t *request; ++ if (!value || !*value) return (-1); ++ while (*value) ++ { ++ long number; ++ errno = 0; ++ number = strtol(value, &end, 10); ++ if (errno == ERANGE || end == value || (*end && *end != ',') || ++ (number != 3 && number != 11 && number != 60 && number != 63) || count == 32) ++ return (-1); ++ values[count ++] = (int)number; ++ if (!*end) break; ++ value = end + 1; ++ if (!*value) return (-1); ++ } ++ request = ippNew(); ++ result = _papplTrimSelection(attrs, ippAddIntegers(request, IPP_TAG_JOB, IPP_TAG_ENUM, "finishings", count, values)); ++ ippDelete(request); ++ return (result); ++} ++ ++static inline bool ++_papplSameTrimSelection(int a, int b) ++{ ++ return ((a == 11 ? 60 : a) == (b == 11 ? 60 : b)); ++} ++ ++static inline int ++_papplTrimDefault(ipp_t *attrs, ipp_attribute_t *enums, ipp_attribute_t *collection) ++{ ++ int a, b; ++ if (enums && ippGetValueTag(enums) != IPP_TAG_ENUM) return (-1); ++ if (collection && ippGetValueTag(collection) != IPP_TAG_BEGIN_COLLECTION && ippGetValueTag(collection) != IPP_TAG_NOVALUE) return (-1); ++ a = enums ? _papplTrimSelection(attrs, enums) : -1; ++ b = collection ? _papplTrimSelection(attrs, collection) : -1; ++ if ((enums && a <= 0) || (collection && b <= 0)) return (-1); ++ if (enums && collection && !_papplSameTrimSelection(a, b)) return (-1); ++ return (enums ? a : b); ++} ++ ++// Caller holds the printer write lock. Both representations change together. ++static inline void ++_papplSetTrimDefault(ipp_t *attrs, int value) ++{ ++ ipp_attribute_t *old; ++ while ((old = ippFindAttribute(attrs, "finishings-default", IPP_TAG_ZERO)) != NULL) ippDeleteAttribute(attrs, old); ++ while ((old = ippFindAttribute(attrs, "finishings-col-default", IPP_TAG_ZERO)) != NULL) ippDeleteAttribute(attrs, old); ++ ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-default", value); ++ if (value == 3) ++ ippAddOutOfBand(attrs, IPP_TAG_PRINTER, IPP_TAG_NOVALUE, "finishings-col-default"); ++ else ++ { ++ ipp_t *col = ippNew(); ++ ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, ippEnumString("finishings", value)); ++ ippAddCollection(attrs, IPP_TAG_PRINTER, "finishings-col-default", col); ++ ippDelete(col); ++ } ++} ++#endif +--- a/pappl/system-loadsave.c ++++ b/pappl/system-loadsave.c +@@ -8,6 +8,7 @@ + // + + #include "pappl-private.h" ++#include "finishings-private.h" + + + // +@@ -235,6 +236,18 @@ + sscanf(value, "%dx%ddpi", &printer->driver_data.x_default, &printer->driver_data.y_default); + else if (!strcasecmp(line, "sides-default")) + printer->driver_data.sides_default = _papplSidesValue(value); ++ else if (!strcasecmp(line, "finishings-default") && _papplTrimPresets(printer->driver_attrs)) ++ { ++ int trim_default = _papplTrimDefaultString(printer->driver_attrs, value); ++ if (trim_default > 0) ++ { ++ _papplRWLockWrite(printer); ++ _papplSetTrimDefault(printer->driver_attrs, trim_default); ++ _papplRWUnlock(printer); ++ } ++ else ++ papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, "Invalid finishings-default in state file."); ++ } + else if ((ptr = strstr(line, "-default")) != NULL) + { + char defname[128], // xxx-default name +@@ -567,10 +580,16 @@ + if (printer->driver_data.sides_default) + cupsFilePutConf(fp, "sides-default", _papplSidesString(printer->driver_data.sides_default)); + ++ if (_papplTrimPresets(printer->driver_attrs)) ++ cupsFilePrintf(fp, "finishings-default %d\n", ippGetInteger(ippFindAttribute(printer->driver_attrs, "finishings-default", IPP_TAG_ENUM), 0)); ++ + for (j = 0; j < (cups_len_t)printer->driver_data.num_vendor; j ++) + { + char defname[128], // xxx-default name + defvalue[1024]; // xxx-default value ++ ++ if (_papplTrimPresets(printer->driver_attrs) && !strcmp(printer->driver_data.vendor[j], "finishings")) ++ continue; + + snprintf(defname, sizeof(defname), "%s-default", printer->driver_data.vendor[j]); + ippAttributeString(ippFindAttribute(printer->driver_attrs, defname, IPP_TAG_ZERO), defvalue, sizeof(defvalue)); +--- a/pappl/printer-webif.c ++++ b/pappl/printer-webif.c +@@ -9,6 +9,7 @@ + // + + #include "pappl-private.h" ++#include "finishings-private.h" + + + // +@@ -343,7 +344,8 @@ + } + } + +- if (data.finishings) ++ // The legacy form cannot represent the explicit trim presets. ++ if (data.finishings && !_papplTrimPresets(printer->driver_attrs)) + { + char name[256], // Form variable name + finishings[256],// Finishings value(s) diff --git a/patches/pappl-1.4.11-raster.patch b/patches/pappl-1.4.11-raster.patch new file mode 100644 index 0000000..856e7bd --- /dev/null +++ b/patches/pappl-1.4.11-raster.patch @@ -0,0 +1,289 @@ +--- a/pappl/job-process.c ++++ b/pappl/job-process.c +@@ -586,6 +586,7 @@ + y; // Current line + int job_pages_per_set; + // "job-pages-per-set" value, if any ++ bool started = false; // Started driver callbacks + unsigned next_copy; // Next copy boundary + + +@@ -598,7 +599,7 @@ + // Open the raster stream... + if ((ras = cupsRasterOpenIO((cups_raster_cb_t)httpRead, client->http, CUPS_RASTER_READ)) == NULL) + { +- papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unable to open raster stream from client - %s", cupsGetErrorString()); ++ papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unable to open raster stream from client - %s", cupsRasterGetErrorString()); + job->state = IPP_JSTATE_ABORTED; + goto complete_job; + } +@@ -606,7 +607,7 @@ + // Prepare options... + if (!cupsRasterReadHeader(ras, &header)) + { +- papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unable to read raster stream from client - %s", cupsGetErrorString()); ++ papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unable to read raster stream from client - %s", cupsRasterGetErrorString()); + job->state = IPP_JSTATE_ABORTED; + goto complete_job; + } +@@ -636,6 +637,8 @@ + goto complete_job; + } + ++ started = true; ++ + // Print pages... + do + { +@@ -652,7 +655,13 @@ + papplJobDeletePrintOptions(options); + options = papplJobCreatePrintOptions(job, (unsigned)job->impressions, header.cupsBitsPerPixel > 8); + +- if (header.cupsWidth == 0 || header.cupsHeight == 0 || (header.cupsBitsPerColor != 1 && header.cupsBitsPerColor != 8) || header.cupsColorOrder != CUPS_ORDER_CHUNKED || (header.cupsBytesPerLine != ((header.cupsWidth * header.cupsBitsPerPixel + 7) / 8))) ++ if (header.cupsWidth == 0 || header.cupsHeight == 0 || ++ header.cupsWidth > 0x00ffffff || header.cupsHeight > 0x00ffffff || ++ header.HWResolution[0] == 0 || header.HWResolution[1] == 0 || ++ header.cupsBitsPerPixel == 0 || header.cupsBitsPerPixel > 32 || ++ (header.cupsBitsPerColor != 1 && header.cupsBitsPerColor != 8) || ++ header.cupsColorOrder != CUPS_ORDER_CHUNKED || ++ header.cupsBytesPerLine != ((header.cupsWidth * header.cupsBitsPerPixel + 7) / 8)) + { + papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Bad raster data seen."); + papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_FORMAT_ERROR, PAPPL_JREASON_NONE); +@@ -668,8 +677,78 @@ + break; + } + +- if (options->header.cupsBitsPerPixel >= 8 && header.cupsBitsPerPixel >= 8) +- options->header = header; // Use page header from client ++ ++ if (options->header.cupsBitsPerPixel == 1 && ++ ((header.cupsBitsPerPixel != 1 && header.cupsBitsPerPixel != 8) || ++ (header.cupsColorSpace != CUPS_CSPACE_K && header.cupsColorSpace != CUPS_CSPACE_W && header.cupsColorSpace != CUPS_CSPACE_SW))) ++ { ++ papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unsupported raster conversion to 1-bit output."); ++ papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); ++ job->state = IPP_JSTATE_ABORTED; ++ break; ++ } ++ ++ // A submitted resolution must be one of this driver's supported pairs. ++ for (x = 0; x < (unsigned)printer->driver_data.num_resolution; x ++) ++ { ++ if (header.HWResolution[0] == (unsigned)printer->driver_data.x_resolution[x] && ++ header.HWResolution[1] == (unsigned)printer->driver_data.y_resolution[x]) ++ break; ++ } ++ if (x == (unsigned)printer->driver_data.num_resolution) ++ { ++ papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unsupported raster resolution."); ++ papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); ++ job->state = IPP_JSTATE_ABORTED; ++ break; ++ } ++ ++ // Keep the page geometry even when converting grayscale to device bits. ++ // Copying only headers with >=8-bit output used the default size otherwise. ++ { ++ cups_page_header_t output = options->header; ++ ipp_attribute_t *attr = ippFindAttribute(job->attrs, "media", IPP_TAG_ZERO); ++ ipp_t *media_col = ippGetCollection(ippFindAttribute(job->attrs, "media-col", IPP_TAG_BEGIN_COLLECTION), 0); ++ ipp_t *size = ippGetCollection(ippFindAttribute(media_col, "media-size", IPP_TAG_BEGIN_COLLECTION), 0); ++ double width = 2540.0 * header.cupsWidth / header.HWResolution[0]; ++ double length = 2540.0 * header.cupsHeight / header.HWResolution[1]; ++ // Media import prefers a concrete size-name over nested dimensions. ++ bool named_media = ippFindAttribute(media_col, "media-size-name", IPP_TAG_ZERO) != NULL; ++ bool explicit_width = attr || (named_media && options->media.size_width > 0) || ippFindAttribute(size, "x-dimension", IPP_TAG_INTEGER); ++ bool explicit_length = attr || (named_media && options->media.size_length > 0) || ippFindAttribute(size, "y-dimension", IPP_TAG_INTEGER); ++ ++ if (width > INT_MAX - 1.0 || length > INT_MAX - 1.0 || ++ (explicit_width && (width < options->media.size_width - 2540.0 / header.HWResolution[0] || width > options->media.size_width + 2540.0 / header.HWResolution[0])) || ++ (explicit_length && (length < options->media.size_length - 2540.0 / header.HWResolution[1] || length > options->media.size_length + 2540.0 / header.HWResolution[1]))) ++ { ++ papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Raster page contradicts explicit job media dimensions."); ++ papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); ++ job->state = IPP_JSTATE_ABORTED; ++ break; ++ } ++ ++ options->header = header; ++ if (output.cupsBitsPerPixel == 1 && header.cupsBitsPerPixel <= 8) ++ { ++ options->header.cupsBitsPerColor = output.cupsBitsPerColor; ++ options->header.cupsBitsPerPixel = output.cupsBitsPerPixel; ++ options->header.cupsColorSpace = output.cupsColorSpace; ++ options->header.cupsNumColors = output.cupsNumColors; ++ options->header.cupsBytesPerLine = (header.cupsWidth + 7) / 8; ++ } ++ else if (header.cupsBitsPerPixel < 8 && output.cupsBitsPerPixel != header.cupsBitsPerPixel) ++ { ++ papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unsupported raster bit-depth conversion."); ++ papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); ++ job->state = IPP_JSTATE_ABORTED; ++ break; ++ } ++ ++ if (!explicit_width) ++ options->media.size_width = (int)(width + 0.5); ++ if (!explicit_length) ++ options->media.size_length = (int)(length + 0.5); ++ } + + if (!(printer->driver_data.rstartpage_cb)(job, options, job->printer->device, page)) + { +@@ -714,7 +793,7 @@ + + for (y = 0; !job->is_canceled && y < header.cupsHeight && y < options->header.cupsHeight; y ++) + { +- if (cupsRasterReadPixels(ras, pixels, header.cupsBytesPerLine)) ++ if (cupsRasterReadPixels(ras, pixels, header.cupsBytesPerLine) == header.cupsBytesPerLine) + { + if (header.cupsBitsPerPixel == 8 && options->header.cupsBitsPerPixel == 1) + { +@@ -765,55 +844,41 @@ + *lineptr = byte; + } + +- (printer->driver_data.rwriteline_cb)(job, options, job->printer->device, y, line); ++ if (!(printer->driver_data.rwriteline_cb)(job, options, job->printer->device, y, line)) ++ { ++ job->state = IPP_JSTATE_ABORTED; ++ break; ++ } + } + else +- (printer->driver_data.rwriteline_cb)(job, options, job->printer->device, y, pixels); +- } +- else +- break; +- } +- +- if (!job->is_canceled && y < header.cupsHeight) +- { +- // Discard excess lines from client... +- while (y < header.cupsHeight) +- { +- cupsRasterReadPixels(ras, pixels, header.cupsBytesPerLine); +- y ++; +- } +- } +- else +- { +- // Pad missing lines with whitespace... +- if (header.cupsBitsPerPixel == 8 && options->header.cupsBitsPerPixel == 1) +- { +- memset(line, 0, options->header.cupsBytesPerLine); +- +- while (y < options->header.cupsHeight) + { +- (printer->driver_data.rwriteline_cb)(job, options, job->printer->device, y, line); +- y ++; ++ if (header.cupsBitsPerPixel == 1 && header.cupsColorSpace != options->header.cupsColorSpace) ++ { ++ for (x = 0; x < header.cupsBytesPerLine; x ++) ++ pixels[x] ^= 255; ++ } ++ if (!(printer->driver_data.rwriteline_cb)(job, options, job->printer->device, y, pixels)) ++ { ++ job->state = IPP_JSTATE_ABORTED; ++ break; ++ } + } + } + else + { +- if (header.cupsColorSpace == CUPS_CSPACE_K || header.cupsColorSpace == CUPS_CSPACE_CMYK) +- memset(pixels, 0x00, header.cupsBytesPerLine); +- else +- memset(pixels, 0xff, header.cupsBytesPerLine); +- +- while (y < options->header.cupsHeight) +- { +- (printer->driver_data.rwriteline_cb)(job, options, job->printer->device, y, pixels); +- y ++; +- } ++ papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Incomplete raster page at row %u.", y); ++ papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_FORMAT_ERROR, PAPPL_JREASON_NONE); ++ job->state = IPP_JSTATE_ABORTED; ++ break; + } + } + + free(pixels); + free(line); + ++ if (job->state == IPP_JSTATE_ABORTED || job->is_canceled) ++ break; ++ + if (!(printer->driver_data.rendpage_cb)(job, options, job->printer->device, page)) + { + job->state = IPP_JSTATE_ABORTED; +@@ -842,19 +907,33 @@ + } + while (cupsRasterReadHeader(ras, &header)); + ++ // CUPS distinguishes a malformed/partial next header from clean stream EOF. ++ if (job->state != IPP_JSTATE_ABORTED && !job->is_canceled) ++ { ++ const char *error = cupsRasterGetErrorString(); ++ if (error && *error) ++ { ++ papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unable to finish raster stream - %s", error); ++ papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_FORMAT_ERROR, PAPPL_JREASON_NONE); ++ job->state = IPP_JSTATE_ABORTED; ++ } ++ } ++ + if (next_copy == 0) + { + // Not tracking copies so record this as a single completed copy... + papplJobSetCopiesCompleted(job, 1); + } + +- if (!(printer->driver_data.rendjob_cb)(job, options, job->printer->device)) +- job->state = IPP_JSTATE_ABORTED; +- else if (header_pages == 0 && job_pages_per_set == 0) ++ if (job->state != IPP_JSTATE_ABORTED && !job->is_canceled && header_pages == 0 && job_pages_per_set == 0) + papplJobSetImpressions(job, (int)page); + + complete_job: + ++ // Cleanup sees aborted/canceled state and cannot finalize buffered output. ++ if (started && !(printer->driver_data.rendjob_cb)(job, options, job->printer->device)) ++ job->state = IPP_JSTATE_ABORTED; ++ + papplJobDeletePrintOptions(options); + + if (httpGetState(client->http) == HTTP_STATE_POST_RECV) +--- a/pappl/job-filter.c ++++ b/pappl/job-filter.c +@@ -543,6 +543,9 @@ + } + } + ++ if (job->is_canceled || job->state != IPP_JSTATE_PROCESSING) ++ goto abort_job; ++ + // Trailing blank space... + memset(line, white, options->header.cupsBytesPerLine); + for (; y < (int)options->header.cupsHeight; y ++) +@@ -565,7 +568,8 @@ + papplJobSetCopiesCompleted(job, 1); + } + +- // End the job... ++ // End the job only once, including when the callback itself fails. ++ started = false; + if (!(driver_data.rendjob_cb)(job, options, device)) + { + papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unable to end raster job."); +@@ -580,6 +584,9 @@ + // Abort the job... + abort_job: + ++ if (!job->is_canceled) ++ job->state = IPP_JSTATE_ABORTED; ++ + if (started) + (driver_data.rendjob_cb)(job, options, device); + diff --git a/scripts/build-cups.sh b/scripts/build-cups.sh new file mode 100755 index 0000000..2389672 --- /dev/null +++ b/scripts/build-cups.sh @@ -0,0 +1,37 @@ +#!/bin/sh +# Rebuild only ABI-compatible libraries from Debian's security-patched source. +# Both images call this helper before compiling PAPPL. +set -eu +cups_source_version=2.4.2-3+deb12u9 +cups_patch_file=${1:?usage: build-cups.sh PATCH} +case "$cups_patch_file" in /*) ;; *) echo 'CUPS patch path must be absolute' >&2; exit 1 ;; esac +sed -i 's/^Types: deb$/Types: deb deb-src/' /etc/apt/sources.list.d/debian.sources +apt-get update +installed_version=$(dpkg-query -W -f='${source:Version}' libcups2) +available_version=$(apt-cache show --no-all-versions libcups2 | sed -n 's/^Version: //p') +if test "$installed_version" != "$cups_source_version" || test "$available_version" != "$cups_source_version"; then + echo "Refresh the pinned CUPS patch: installed=$installed_version available=$available_version expected=$cups_source_version" >&2 + exit 1 +fi +mkdir -p /tmp/ptouch-cups-build +cd /tmp/ptouch-cups-build +apt-get source "cups=$cups_source_version" +cd cups-2.4.2 +patch --batch --forward --fuzz=0 -p1 < "$cups_patch_file" +# Retain Debian's library TLS, DNS-SD, Kerberos, and hardening configuration. +export DEB_BUILD_MAINT_OPTIONS=hardening=+all,-pie +export CFLAGS="$(dpkg-buildflags --get CFLAGS)" +export CPPFLAGS="$(dpkg-buildflags --get CPPFLAGS)" +export LDFLAGS="$(dpkg-buildflags --get LDFLAGS)" +export DSOFLAGS="$LDFLAGS" +./configure --prefix=/usr/local --libdir=/usr/local/lib \ + --with-tls=gnutls --with-dnssd=avahi --enable-gssapi --enable-threads +make -C cups libs +make -C cups install-libs +mkdir -p /usr/local/share/ptouch +printf '%s\n' "$cups_source_version" > /usr/local/share/ptouch/cups-source-version +ldconfig +# No scheduler, filters, clients, or new headers are installed. +cd /tmp +rm -r /tmp/ptouch-cups-build +find /var/lib/apt/lists -type f -delete diff --git a/scripts/check-cups.py b/scripts/check-cups.py new file mode 100644 index 0000000..4eda316 --- /dev/null +++ b/scripts/check-cups.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python3 +"""Run inside an isolated CUPS-equipped container, without USB or external network. + +Usage: python3 scripts/check-cups.py /path/to/ptouch-app +Requires cupsd, cups-filters, Ghostscript, ipptool, and Pillow. +""" +import importlib.util, os, pathlib, subprocess, tempfile, time, socket, sys, re, urllib.request, urllib.parse, http.cookiejar +from PIL import Image, ImageDraw +spec=importlib.util.spec_from_file_location("smoke", str(pathlib.Path(__file__).with_name("smoke-image.py"))) +smoke=importlib.util.module_from_spec(spec); spec.loader.exec_module(smoke) +run=smoke.run +with tempfile.TemporaryDirectory(prefix="cups-queue-") as temp: + d=pathlib.Path(temp) + reserved=socket.socket() + reserved.bind(("127.0.0.1",0)) + sink_port=reserved.getsockname()[1] + sink=None + env=dict(os.environ, PTOUCH_DEVICE_URI=f"socket://127.0.0.1:{sink_port}", CUPS_SERVER="127.0.0.1:8631") + os.environ["CUPS_SERVER"]=env["CUPS_SERVER"] + conf=d/"cupsd.conf" + conf.write_text("""LogLevel debug +Listen 127.0.0.1:8631 +Browsing Off +WebInterface No +DefaultAuthType None + + Order allow,deny + Allow from localhost + + + Order allow,deny + Allow from localhost + + + + Order allow,deny + Allow from localhost + + +""") + app_log=open(d/"app.log", "w") + cups_log=open(d/"cups.log", "w") + app=subprocess.Popen([sys.argv[1], "server"], env=env, stdout=app_log, stderr=app_log) + cups=subprocess.Popen(["cupsd", "-f", "-c", str(conf)], env=env, stdout=cups_log, stderr=cups_log) + try: + url="ipp://127.0.0.1:8000/ipp/print/ptouch" + query=smoke.operation("Get-Printer-Attributes", "ATTR keyword requested-attributes all", "EXPECT finishings-default WITH-VALUE 11") + for attempt in range(20): + try: + print(smoke.ipp(url,d,query)); break + except RuntimeError: + if attempt == 19: + raise + time.sleep(1) + # Existing Printing Defaults form must render and preserve canonical cuts. + browser=urllib.request.build_opener(urllib.request.HTTPCookieProcessor(http.cookiejar.CookieJar())) + weburl="http://127.0.0.1:8000/ptouch/printing" + for default in (11,63): + print(smoke.ipp(url,d,smoke.operation("Set-Printer-Attributes", + "GROUP printer-attributes-tag\n ATTR enum finishings-default "+str(default),""))) + html=browser.open(weburl).read().decode() + token=re.search(r'name="session" value="([^"]+)"',html).group(1) + form=urllib.parse.urlencode({"session":token,"print-quality":"normal"}).encode() + html=browser.open(weburl,form).read().decode() + assert "Changes saved." in html, html + print(smoke.ipp(url,d,smoke.operation("Get-Printer-Attributes", + "ATTR keyword requested-attributes finishings-default", "EXPECT finishings-default WITH-VALUE "+str(default)))) + print(smoke.ipp(url,d,smoke.operation("Set-Printer-Attributes", + "GROUP printer-attributes-tag\n ATTR enum finishings-default 11",""))) + print(run("lpadmin", "-p", "ptouch-cups", "-E", "-v", url, "-m", "everywhere")) + print(run("lpoptions", "-p", "ptouch-cups", "-o", "finishings=11")) + print(run("lpoptions", "-p", "ptouch-cups", "-l")) + png=d/"label.png"; smoke.png(png) + Image.open(png).save(d/"label.jpg", dpi=(180,180), quality=95) + pdf=d/"labels.pdf" + stream1=b"1 w 3 3 28 125 re S 8 20 4 4 re f\n" + stream2=b"1 w 3 3 28 125 re S 8 35 4 4 re f\n" + objects=[b"<< /Type /Catalog /Pages 2 0 R >>", b"<< /Type /Pages /Kids [3 0 R 4 0 R] /Count 2 >>", + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 34.015748 141.732283] /Resources << >> /Contents 5 0 R >>", + b"<< /Type /Page /Parent 2 0 R /MediaBox [0 0 34.015748 141.732283] /Resources << >> /Contents 6 0 R >>", + b"<< /Length "+str(len(stream1)).encode()+b" >>\nstream\n"+stream1+b"endstream", + b"<< /Length "+str(len(stream2)).encode()+b" >>\nstream\n"+stream2+b"endstream"] + data=bytearray(b"%PDF-1.4\n"); offsets=[0] + for number,obj in enumerate(objects,1): + offsets.append(len(data)); data.extend(str(number).encode()+b" 0 obj\n"+obj+b"\nendobj\n") + start=len(data); data.extend(b"xref\n0 7\n0000000000 65535 f \n") + for offset in offsets[1:]: data.extend(f"{offset:010d} 00000 n \n".encode()) + data.extend(f"trailer\n<< /Size 7 /Root 1 0 R >>\nstartxref\n{start}\n%%EOF\n".encode()); pdf.write_bytes(data) + # Two real CUPS jobs wait while the printer socket refuses connections. + for finishing in ("60","63"): + print(run("lp","-d","ptouch-cups","-o","media=om_12x50mm_12x50mm", + "-o","print-scaling=none","-o","finishings="+finishing,str(png))) + time.sleep(1) + waiting=run("lpstat","-o","ptouch-cups") + assert waiting.count("ptouch-cups-")==2, waiting + print("Offline jobs:",waiting) + reserved.close() + sink=smoke.Sink(sink_port) + for number in range(2): + result=smoke.summary(smoke.decode(sink.take(timeout=45))) + assert len(result["pages"])==1 and result["pages"][0]["rows"]==354 and result["eject"]==1, result + print("Resumed offline job",number+1,result) + # PNG/JPEG and a multi-label PDF through an ordinary driverless queue. + for file, finishing, count, copies in ((png,None,1,1),(png,"60",1,1),(d/"label.jpg","63",1,1),(pdf,"60",2,1),(pdf,"63",2,1),(pdf,"60",4,2)): + args=["lp", "-d", "ptouch-cups", "-n", str(copies), "-o", "media=om_12x50mm_12x50mm", "-o", "print-scaling=none"] + if finishing: args += ["-o", "finishings="+finishing] + print(run(*args,str(file))) + data=sink.take() + result=smoke.summary(smoke.decode(data)); print("CUPS",file.name,finishing,result) + if not result["pages"]: + print(smoke.ipp(url,d,smoke.operation("Get-Job-Attributes", "ATTR integer job-id 1", "EXPECT job-state"))) + assert len(result["pages"])==count, result + assert [p["rows"] for p in result["pages"]]==[354]*count, result + assert result["eject"]==1, result + assert [p["autocut"] for p in result["pages"]]==([True]*count if finishing!="63" else [True]+[False]*(count-1)), result + assert not result["terminal_commands_after_pages"], result + # A legacy CUPS PPD has one global custom margin, the maximum across + # tapes. Honor it: a compact source fits; a full 76-dot source does not. + custom=d/"custom.png" + image=Image.new("L",(42,180),255) + ImageDraw.Draw(image).rectangle((0,0,41,179),outline=0) + image.save(custom,dpi=(180,180)) + print(run("lp","-d","ptouch-cups","-o","media=Custom.12x40mm", + "-o","print-scaling=none",str(custom))) + result=smoke.summary(smoke.decode(sink.take(timeout=45))) + assert len(result["pages"])==1 and result["pages"][0]["rows"]==282 and result["eject"]==1, result + print("CUPS rounded custom size with explicit margins:",result) + print(run("lp","-d","ptouch-cups","-o","media=Custom.12x40mm", + "-o","print-scaling=none",str(png))) + assert sink.take(timeout=45)==b"", "Oversized custom PNG produced output" + assert "PNG content is 76 dots across tape; printable width is 42" in (d/"app.log").read_text(), "Missing geometry rejection" + print("CUPS oversized custom PNG rejected without output.") + # Exact custom dimensions and tape margins use ordinary media-col. + request=smoke.operation("Print-Job", f"""ATTR mimeMediaType document-format image/png + GROUP job-attributes-tag + ATTR keyword print-scaling none + ATTR collection media-col {{ + MEMBER collection media-size {{ MEMBER integer x-dimension 1200 MEMBER integer y-dimension 4000 }} + MEMBER integer media-left-margin 63 + MEMBER integer media-right-margin 71 + }} + FILE "{png}" + """, "EXPECT job-id WITH-VALUE >0 DEFINE-VALUE job-id") + request+=smoke.operation("Get-Job-Attributes", "ATTR integer job-id $job-id", + "EXPECT job-state WITH-VALUE >5 REPEAT-NO-MATCH REPEAT-LIMIT 50\n EXPECT job-state WITH-VALUE 9") + print(smoke.ipp(url,d,request)) + result=smoke.summary(smoke.decode(sink.take())) + assert len(result["pages"])==1 and result["pages"][0]["rows"]==283 and result["eject"]==1, result + print("Exact standard custom media-col:",result) + # Chain-only travels through the standard IPP collection interface. + print(smoke.submit(url,d,png,"ATTR collection finishings-col { MEMBER keyword finishing-template ptouch-chain }")) + result=smoke.summary(smoke.decode(sink.take())) + assert len(result["pages"])==1 and result["pages"][0]["rows"]==354 and result["eject"]==0, result + assert result["pages"][0]["autocut"], result + print("Standard ptouch-chain collection:",result) + print("Conventional CUPS integration passed.") + finally: + for process in (cups,app): + process.terminate() + try: process.wait(timeout=5) + except subprocess.TimeoutExpired: process.kill(); process.wait() + reserved.close() + if sink: sink.socket.close() + app_log.close(); cups_log.close() + print((d/"app.log").read_text(errors="replace")) + print((d/"cups.log").read_text(errors="replace")) + if sys.exc_info()[0] and pathlib.Path("/var/log/cups/error_log").exists(): + print(pathlib.Path("/var/log/cups/error_log").read_text(errors="replace")) diff --git a/scripts/smoke-image.py b/scripts/smoke-image.py new file mode 100644 index 0000000..eaab29d --- /dev/null +++ b/scripts/smoke-image.py @@ -0,0 +1,191 @@ +#!/usr/bin/env python3 +"""Exercise the deploy image through IPP and a socket sink, without USB access.""" +import argparse +import json +from pathlib import Path +import queue +import socket +import struct +import subprocess +import sys +import tempfile +import threading +import time +import uuid +import zlib + +sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "tests")) +from test_cut_probe import decode, summary + + +def run(*args): + return subprocess.run(args, check=True, text=True, stdout=subprocess.PIPE, + stderr=subprocess.STDOUT).stdout + + +def png(path): + def chunk(name, payload): + return (struct.pack("!I", len(payload)) + name + payload + + struct.pack("!I", zlib.crc32(name + payload))) + # A 76-dot imageable design on 12 mm tape, with explicit 180 dpi metadata. + pixels = b"".join(b"\0" + bytes(0 if x in (0, 75) or y in (0, 179) else 255 + for x in range(76)) for y in range(180)) + path.write_bytes(b"\x89PNG\r\n\x1a\n" + + chunk(b"IHDR", struct.pack("!2I5B", 76, 180, 8, 0, 0, 0, 0)) + + chunk(b"pHYs", struct.pack("!2IB", 7087, 7087, 1)) + + chunk(b"IDAT", zlib.compress(pixels)) + chunk(b"IEND", b"")) + + +class Sink: + def __init__(self, port=0): + self.socket = socket.socket() + self.socket.bind(("0.0.0.0", port)) + self.socket.listen() + self.port = self.socket.getsockname()[1] + self.jobs = queue.Queue() + self.thread = threading.Thread(target=self.capture, daemon=True) + self.thread.start() + + def capture(self): + while True: + try: + connection, _ = self.socket.accept() + except OSError: + return + with connection: + data = bytearray() + while block := connection.recv(65536): + data.extend(block) + self.jobs.put(bytes(data)) + + def take(self, timeout=10): + return self.jobs.get(timeout=timeout) + + +def operation(name, body, expect): + return f'''{{ + OPERATION {name} + GROUP operation-attributes-tag + ATTR charset attributes-charset utf-8 + ATTR naturalLanguage attributes-natural-language en + ATTR uri printer-uri $uri + ATTR name requesting-user-name smoke + {body} + STATUS successful-ok + {expect} +}} +''' + + +def ipp(url, directory, text): + path = directory / "request.test" + path.write_text(text) + try: + return run("ipptool", "-tv", "-T", "10", url, str(path)) + except subprocess.CalledProcessError as error: + raise RuntimeError(error.stdout) from error + + +def submit(url, directory, file, finishing="", copies=1, state=9): + request = operation("Print-Job", f'''ATTR mimeMediaType document-format image/png + GROUP job-attributes-tag + ATTR integer copies {copies} + ATTR keyword media om_12x50mm_12x50mm + ATTR keyword print-scaling none + {finishing} + FILE "{file}"''', "EXPECT job-id WITH-VALUE >0 DEFINE-VALUE job-id") + request += operation("Get-Job-Attributes", "ATTR integer job-id $job-id", + f'''EXPECT job-state WITH-VALUE >5 REPEAT-NO-MATCH REPEAT-LIMIT 50 + EXPECT job-state WITH-VALUE {state}''') + return ipp(url, directory, request) + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("image", help="already-built deploy image") + args = parser.parse_args() + name = "ptouch-smoke-" + uuid.uuid4().hex[:10] + sink = Sink() + try: + run("docker", "run", "-d", "--name", name, + "--add-host", "host.docker.internal:host-gateway", + "-p", "127.0.0.1::8000", "-e", + f"PTOUCH_DEVICE_URI=socket://host.docker.internal:{sink.port}", args.image) + mapping = json.loads(run("docker", "inspect", "--format", + "{{json .NetworkSettings.Ports}}", name)) + url = f"ipp://127.0.0.1:{mapping['8000/tcp'][0]['HostPort']}/ipp/print/ptouch" + with tempfile.TemporaryDirectory(prefix="ptouch-smoke-") as temp: + directory = Path(temp) + image = directory / "label.png" + png(image) + attributes = operation("Get-Printer-Attributes", + "ATTR keyword requested-attributes all", ''' + EXPECT finishings-supported COUNT 4 WITH-ALL-VALUES 3,11,60,63 + EXPECT finishings-default WITH-VALUE 11 + EXPECT finishings-col-database OF-TYPE collection + EXPECT finishing-template-supported WITH-VALUE ptouch-chain + EXPECT document-format-supported WITH-VALUE image/png WITH-DISTINCT-VALUES + EXPECT document-format-supported WITH-ALL-VALUES "/^(application/octet-stream|image/(pwg-raster|urf|png|jpeg))$/"''') + for attempt in range(20): + try: + print(ipp(url, directory, attributes)) + break + except RuntimeError: + if attempt == 19: + raise + time.sleep(1) + cases = ( + ("default", "", [True] * 3, True, 1), + ("none", "ATTR enum finishings 3", [True, False, False], False, 1), + ("chain", '''ATTR collection finishings-col { + MEMBER keyword finishing-template ptouch-chain + }''', [True] * 3, False, 0), + ("each", "ATTR enum finishings 60", [True] * 3, True, 0), + ("end", "ATTR enum finishings 63", [True, False, False], True, 1), + ) + for label, finishing, autocuts, at_end, resets in cases: + print(submit(url, directory, image, finishing, copies=3)) + result = summary(decode(sink.take())) + pages = result["pages"] + assert len(pages) == 3, result + assert [p["rows"] for p in pages] == [354] * 3, result + assert [p["autocut"] for p in pages] == autocuts, result + assert [p["chain_off_at_print"] for p in pages] == [False, False, at_end], result + assert [p["finish"] for p in pages] == ["ff", "ff", "eject" if at_end else "ff"], result + assert result["init"] == resets, result + assert result["eject"] == int(at_end), result + assert not result["terminal_commands_after_pages"], result + print(label, json.dumps(result)) + # Printer defaults use the same standard enum/collection boundary. + for value, out_type, at_end in ((63, "collection", True), (3, "no-value", False)): + # Admin IPP operations require local access. The existing CLI + # issues Set-Printer-Attributes through the container's socket. + print(run("docker", "exec", name, "ptouch-app", "modify", "-d", "ptouch", + "-o", f"finishings={value}")) + print(ipp(url, directory, operation("Get-Printer-Attributes", + "ATTR keyword requested-attributes finishings-default,finishings-col-default", + f'''EXPECT finishings-default WITH-VALUE {value} + EXPECT finishings-col-default OF-TYPE {out_type}'''))) + print(submit(url, directory, image, copies=2)) + result = summary(decode(sink.take())) + assert [p["rows"] for p in result["pages"]] == [354, 354], result + assert [p["autocut"] for p in result["pages"]] == [True, False], result + assert [p["chain_off_at_print"] for p in result["pages"]] == [False, at_end], result + assert [p["finish"] for p in result["pages"]] == ["ff", "eject" if at_end else "ff"], result + assert result["init"] == 1 and not result["terminal_commands_after_pages"], result + print("default", value, json.dumps(result)) + malformed = directory / "malformed.png" + malformed.write_bytes(b"\x89PNG\r\n\x1a\n") + print(submit(url, directory, malformed, state=8)) + wire = sink.take() + assert wire == b"", "Malformed PNG emitted printer commands" + print("Image smoke passed: four presets, synchronized defaults, geometry, copies, and input failure.") + finally: + subprocess.run(["docker", "logs", name], check=False) + subprocess.run(["docker", "rm", "-f", name], check=False, + stdout=subprocess.DEVNULL) + sink.socket.close() + + +if __name__ == "__main__": + main() diff --git a/src/cutter.c b/src/cutter.c index 5323312..7ec837c 100644 --- a/src/cutter.c +++ b/src/cutter.c @@ -1,47 +1,14 @@ #include "cutter.h" -int pt_plan_batch(int n_pages, uint8_t tape_mm, pt_cut_mode mode, int precut, - bool chain_out, pt_op *out, int cap) +pt_cut_policy pt_cut_policy_for(pt_cut_mode mode) { - int k = 0; -#define EMIT(K, A, P) \ - do { \ - if (k >= cap) return -1; \ - out[k].kind = (K); \ - out[k].arg = (uint8_t)(A); \ - out[k].page_index = (P); \ - k++; \ - } while (0) - - for (int i = 0; i < n_pages; i++) { - /* Two distinct meanings, deliberately not one flag: `last_page` is only - * about indexing this batch, `terminal` is "last label of the whole run". - * Folding chain_out into `final` would leave `do_precut` forcing autocut - * on for PT_CUT_END, turning `end` into `each` at every job boundary. */ - int last_page = (i == n_pages - 1); - int terminal = last_page && !chain_out; - int cut_after = (mode == PT_CUT_EACH) || (mode == PT_CUT_END && terminal); - int do_precut = precut && (mode != PT_CUT_NONE); - /* Autocut separates the preceding label from this one. With no - * precut, leave it off only for the first copy, not the last copy. */ - int needs_auto_cut = do_precut || (mode == PT_CUT_EACH && i > 0); - - if (needs_auto_cut) { - EMIT(PT_OP_PRINTINFO, tape_mm, i); - } - EMIT(PT_OP_SETMODE, needs_auto_cut ? 0x40 : 0x00, i); - - int want_chain = (!terminal) || !cut_after; - EMIT(PT_OP_SETADVANCED, want_chain ? 0x00 : 0x08, i); - - EMIT(PT_OP_PRINT_PAGE, 0, i); + return (pt_cut_policy){ + .cut_between = mode == PT_CUT_CHAIN || mode == PT_CUT_EACH, + .cut_at_end = mode == PT_CUT_EACH || mode == PT_CUT_END, + }; +} - if (cut_after && terminal) { - EMIT(PT_OP_EJECT, 0, i); - } else { - EMIT(PT_OP_FF, 0, i); - } - } - return k; -#undef EMIT +bool pt_cut_autocut(pt_cut_policy policy, bool first_label) +{ + return first_label || policy.cut_between; } diff --git a/src/cutter.h b/src/cutter.h index 0cb0878..be64e0a 100644 --- a/src/cutter.h +++ b/src/cutter.h @@ -2,40 +2,24 @@ #define PT_CUTTER_H #include -#include typedef enum { - PT_CUT_EACH, /* precut + cut after every label (chained between) */ - PT_CUT_END, /* cut only after the final label */ - PT_CUT_NONE, /* no cuts */ + PT_CUT_EACH, /* cuts between labels and after the batch */ + PT_CUT_END, /* cut only after the batch */ + PT_CUT_NONE, /* no label cuts; leave the batch open */ + PT_CUT_CHAIN, /* cuts between labels; leave the batch open */ } pt_cut_mode; -typedef enum { - PT_OP_PRINTINFO, - PT_OP_SETMODE, - PT_OP_SETADVANCED, - PT_OP_PRINT_PAGE, - PT_OP_FF, - PT_OP_EJECT, -} pt_op_kind; - typedef struct { - pt_op_kind kind; - uint8_t arg; /* PRINTINFO: tape mm; SETMODE/SETADVANCED: flags */ - int page_index; /* page this op belongs to */ -} pt_op; + bool cut_between; + bool cut_at_end; +} pt_cut_policy; + +/* Pure policy, independent of copies or total label count. */ +pt_cut_policy pt_cut_policy_for(pt_cut_mode mode); -/* Plan the op sequence for an N-page batch based on the macOS-fork - * `flush_print_job` logic, with PT-2730 copy-boundary corrections. - * N comes from PAPPL pages/copies in one device session - * (NOT multi-document). Returns the op count, or -1 if `cap` is too small. - * Pure: the driver renders each op to bytes via protocol.c. - * - * chain_out: another job follows, so plan the LAST label like a non-final one - * (SETADVANCED 0x00, finish FF) and leave the tape at the head instead of - * ejecting (#41). Autocut keeps whatever `mode` + `precut` already produce - - * this flag must not change it. Ignored for PT_CUT_NONE. */ -int pt_plan_batch(int n_pages, uint8_t tape_mm, pt_cut_mode mode, int precut, - bool chain_out, pt_op *out, int cap); +/* Every nonempty batch has mandatory precut at its first actual label. + * Later labels cut the preceding label only when the policy requests it. */ +bool pt_cut_autocut(pt_cut_policy policy, bool first_label); #endif /* PT_CUTTER_H */ diff --git a/src/device_usb.c b/src/device_usb.c index 46c3899..4e069f4 100644 --- a/src/device_usb.c +++ b/src/device_usb.c @@ -19,6 +19,32 @@ #define PT_USB_TIMEOUT_MS 2000 #define PT_USB_STATUS_TIMEOUT_MS 300 +const pt_dev *pt_usb_supported_model(const char *model) +{ + if (!model || !*model) model = "PT-2730"; + return !strcmp(model, "PT-2730") ? pt_lookup_name(model) : NULL; +} + +bool pt_usb_is_uri(const char *uri) +{ + return uri && !strcmp(uri, "ptouch://Brother/PT-2730"); +} + +static ssize_t count_matches(libusb_device **devs, ssize_t n, + uint16_t vid, uint16_t pid, libusb_device **match) +{ + ssize_t count = 0; + for (ssize_t i = 0; i < n; i++) { + struct libusb_device_descriptor d; + if (libusb_get_device_descriptor(devs[i], &d) == 0 && + d.idVendor == vid && d.idProduct == pid) { + count++; + if (match) *match = devs[i]; + } + } + return count; +} + typedef struct { libusb_context *ctx; /* per-connection context, exited on close */ libusb_device_handle *h; @@ -33,7 +59,6 @@ typedef struct { static bool pt_usb_list(pappl_device_cb_t cb, void *data, pappl_deverror_cb_t err_cb, void *err_data) { - (void)err_cb; (void)err_data; libusb_context *ctx = NULL; if (libusb_init(&ctx) != 0) return false; @@ -44,21 +69,26 @@ static bool pt_usb_list(pappl_device_cb_t cb, void *data, return false; } - bool stop = false; - for (ssize_t i = 0; i < n && !stop; i++) { + for (ssize_t i = 0; i < n; i++) { struct libusb_device_descriptor d; if (libusb_get_device_descriptor(devs[i], &d) != 0 || d.idVendor != PT_VID) continue; const pt_dev *dev = pt_lookup_dev(d.idVendor, d.idProduct); - if (!dev) + if (!dev || pt_usb_supported_model(dev->name)) continue; - - char uri[256], id[256], info[128]; - snprintf(uri, sizeof uri, "ptouch://Brother/%s", dev->name); - snprintf(id, sizeof id, "MFG:Brother;MDL:%s;CMD:PT-CBP;", dev->name); - snprintf(info, sizeof info, "%s (USB)", dev->name); - stop = cb(info, uri, id, data); /* cb returns true to stop early */ + char message[160]; + snprintf(message, sizeof message, "%s (%04x:%04x) is recognized but not print-supported", + dev->name, d.idVendor, d.idProduct); + if (err_cb) err_cb(message, err_data); } + const pt_dev *supported = pt_usb_supported_model(NULL); + ssize_t count = count_matches(devs, n, supported->vid, supported->pid, NULL); + bool stop = false; + if (count == 1) + stop = cb("PT-2730 (USB)", "ptouch://Brother/PT-2730", + "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", data); + else if (count > 1 && err_cb) + err_cb("Multiple PT-2730 devices match; USB selection is ambiguous", err_data); libusb_free_device_list(devs, 1); libusb_exit(ctx); return stop; @@ -67,8 +97,10 @@ static bool pt_usb_list(pappl_device_cb_t cb, void *data, static bool pt_usb_open(pappl_device_t *device, const char *device_uri, const char *name) { (void)name; - const char *slash = strrchr(device_uri, '/'); - const char *model = slash ? slash + 1 : device_uri; + if (!pt_usb_is_uri(device_uri)) { + papplDeviceError(device, "Unsupported P-touch device URI: %s", device_uri); + return false; + } libusb_context *ctx = NULL; if (libusb_init(&ctx) != 0) @@ -80,17 +112,19 @@ static bool pt_usb_open(pappl_device_t *device, const char *device_uri, const ch return false; } + const pt_dev *supported = pt_usb_supported_model(NULL); + libusb_device *match = NULL; + ssize_t count = count_matches(devs, n, supported->vid, supported->pid, &match); + if (count != 1) { + papplDeviceError(device, count > 1 ? "Multiple PT-2730 devices match; USB selection is ambiguous" + : "PT-2730 is not attached"); + libusb_free_device_list(devs, 1); + libusb_exit(ctx); + return false; + } libusb_device_handle *h = NULL; int detached = 0; - for (ssize_t i = 0; i < n; i++) { - struct libusb_device_descriptor d; - if (libusb_get_device_descriptor(devs[i], &d) != 0 || d.idVendor != PT_VID) - continue; - const pt_dev *dev = pt_lookup_dev(d.idVendor, d.idProduct); - if (!dev || (model[0] && strcmp(dev->name, model) != 0)) - continue; - - if (libusb_open(devs[i], &h) != 0) { h = NULL; continue; } + if (libusb_open(match, &h) == 0) { if (libusb_kernel_driver_active(h, 0) == 1 && libusb_detach_kernel_driver(h, 0) == 0) detached = 1; @@ -98,9 +132,7 @@ static bool pt_usb_open(pappl_device_t *device, const char *device_uri, const ch if (detached) libusb_attach_kernel_driver(h, 0); libusb_close(h); h = NULL; detached = 0; - continue; } - break; /* success */ } libusb_free_device_list(devs, 1); if (!h) { @@ -151,8 +183,11 @@ static ssize_t pt_usb_read(pappl_device_t *device, void *buffer, size_t bytes) return -1; } -ssize_t pt_usb_read_status(pappl_device_t *device, void *buffer, size_t bytes) +ssize_t pt_usb_read_status(pappl_device_t *device, const char *device_uri, + void *buffer, size_t bytes) { + if (!device || !pt_usb_is_uri(device_uri)) + return -1; pt_usb_conn *c = papplDeviceGetData(device); if (!c) return -1; @@ -195,35 +230,32 @@ static pappl_preason_t pt_usb_status(pappl_device_t *device) static char *pt_usb_id(pappl_device_t *device, char *buffer, size_t bufsize) { (void)device; - strncpy(buffer, "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", bufsize - 1); - buffer[bufsize - 1] = '\0'; + if (bufsize) snprintf(buffer, bufsize, "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;"); return buffer; } -bool pt_usb_present(uint16_t vid, uint16_t pid) +ssize_t pt_usb_count(uint16_t vid, uint16_t pid) { libusb_context *ctx = NULL; if (libusb_init(&ctx) != 0) - return false; + return -1; libusb_device **devs; ssize_t n = libusb_get_device_list(ctx, &devs); if (n < 0) { libusb_exit(ctx); - return false; + return -1; } - bool found = false; - for (ssize_t i = 0; i < n && !found; i++) { - struct libusb_device_descriptor d; - if (libusb_get_device_descriptor(devs[i], &d) != 0) - continue; - if (d.idVendor == vid && d.idProduct == pid) - found = true; /* cached descriptor compare; no device I/O */ - } + ssize_t count = count_matches(devs, n, vid, pid, NULL); libusb_free_device_list(devs, 1); libusb_exit(ctx); - return found; + return count; +} + +bool pt_usb_present(uint16_t vid, uint16_t pid) +{ + return pt_usb_count(vid, pid) == 1; } void pt_usb_register(void) diff --git a/src/device_usb.h b/src/device_usb.h index 619f1a6..34231ba 100644 --- a/src/device_usb.h +++ b/src/device_usb.h @@ -5,18 +5,31 @@ #include #include /* ssize_t */ #include /* pappl_device_t */ +#include "tables.h" + +/* NULL/empty selects the default; recognized models without a print path fail. */ +const pt_dev *pt_usb_supported_model(const char *model); + +/* Exact supported model URI; test/network transports never use USB data. */ +bool pt_usb_is_uri(const char *uri); /* Register the custom "ptouch://" libusb device scheme with PAPPL. * Must be called before any device list/open. */ void pt_usb_register(void); -/* True iff a USB device with exactly this vid:pid is on the bus. +/* Number of matching devices, or -1 if enumeration fails. No device I/O. */ +ssize_t pt_usb_count(uint16_t vid, uint16_t pid); + +/* True iff exactly one USB device with this vid:pid is on the bus. * Enumeration only: no libusb_open, no control transfer to the device. * Used by the status callback to tell "unplugged" from "in use". */ bool pt_usb_present(uint16_t vid, uint16_t pid); /* Short-timeout (300 ms) status read for status_cb; bounds the mute-device * retry loop. Same return convention as the scheme read. */ -ssize_t pt_usb_read_status(pappl_device_t *device, void *buffer, size_t bytes); +/* device_uri must be the owning printer's URI. The guard runs before accessing + * connection data; PAPPL provides no public device URI accessor. */ +ssize_t pt_usb_read_status(pappl_device_t *device, const char *device_uri, + void *buffer, size_t bytes); #endif /* PT_DEVICE_USB_H */ diff --git a/src/driver.c b/src/driver.c index 4cd6abc..7aceb06 100644 --- a/src/driver.c +++ b/src/driver.c @@ -3,6 +3,7 @@ #include #include #include "driver.h" +#include "filter_png.h" #include "tables.h" #include "protocol.h" #include "raster.h" @@ -29,15 +30,6 @@ static const int pt_preset_mm[] = { 25, 50, 100 }; #define PT_MAX_MEDIA_NAMES 64 static char g_media_names[PT_MAX_MEDIA_NAMES][PT_MEDIA_NAME_LEN]; -/* px in 1/100 mm so CUPS' truncating px conversion round-trips exactly. - * cupsRasterInitHeader does cupsWidth = width*dpi/2540 with INTEGER TRUNCATION, - * so round() can drift down by 1 (e.g. 76px->round 1072->floor 75). CEIL of - * px*2540/dpi gives the smallest 1/100 mm whose truncation is exactly px. */ -static int pt_px_to_centimm(int px, int dpi) -{ - return (px * 2540 + dpi - 1) / dpi; -} - /* Extract the model from an IEEE-1284 device-id ("...;MDL:PT-2730;..."). */ static void parse_mdl(const char *device_id, char *out, size_t n) { @@ -75,17 +67,19 @@ static bool pt_dev_write(pappl_device_t *device, const uint8_t *buf, size_t n) * read result, which separates an I/O error (< 0) from no answer at all (0). */ typedef struct { ssize_t n; bool parsed; pt_status st; } pt_live_read; -/* Read the live tape from an already-open device: optional ESC @ + ESC i S, then poll for +/* Read the live tape from an already-open device: ESC i S, then poll for * the reply. The caller owns the device and interprets the result; a supported - * tape is `parsed && st.tape_px > 0`. Callers must have checked that this is a - * ptouch:// device: pt_usb_read_status casts the device data to our libusb - * struct, so calling it on socket:// is undefined behaviour. */ -static pt_live_read pt_read_live_tape(pappl_device_t *device, bool initialize) + * tape is `parsed && st.tape_px > 0`. Validate transport before any command or + * connection-data access; a socket sink has no USB status path. */ +static pt_live_read pt_read_live_tape(pappl_device_t *device, const char *uri) { uint8_t cmd[8]; pt_live_read r = {0}; - if ((initialize && !pt_dev_write(device, cmd, pt_cmd_init(cmd))) || - !pt_dev_write(device, cmd, pt_cmd_status_request(cmd))) { + if (!pt_usb_is_uri(uri)) { + r.n = -1; + return r; + } + if (!pt_dev_write(device, cmd, pt_cmd_status_request(cmd))) { r.n = -1; return r; } @@ -95,287 +89,315 @@ static pt_live_read pt_read_live_tape(pappl_device_t *device, bool initialize) for (int tries = 0; tries < PT_STATUS_POLL_TRIES; tries++) { struct timespec w = {0, PT_STATUS_POLL_NS}; nanosleep(&w, NULL); - ssize_t n = pt_usb_read_status(device, buf + used, sizeof buf - used); + ssize_t n = pt_usb_read_status(device, uri, buf + used, sizeof buf - used); if (n < 0) { r.n = n; break; } used += (size_t)n; r.n = (ssize_t)used; if (used < sizeof buf) continue; /* Earlier page notifications may precede our requested snapshot. * Consume them so they cannot satisfy the next page's readiness wait. */ - r.parsed = buf[18] == 0 && pt_parse_status(buf, &r.st) == 0; - if (r.parsed) break; + if (buf[18] == 0) { + r.parsed = pt_parse_status(buf, &r.st) == 0; + return r; + } used = 0; r.n = 0; } return r; } -#define PT_MAX_LABELS 64 -#define PT_OPS_PER_LABEL 5 -#define PT_OPS_CAP (PT_MAX_LABELS * PT_OPS_PER_LABEL) -typedef struct { pt_op ops[PT_OPS_CAP]; int n_ops; int index; - uint8_t tape_mm; } pt_batch; +/* One table supplies public enum/template aliases and the resolved policy. */ +typedef struct { int value; const char *name; pt_cut_mode mode; } pt_finishing; +static const pt_finishing pt_finishings[] = { + {3, NULL, PT_CUT_NONE}, + {11, "trim", PT_CUT_EACH}, + {60, "trim-after-pages", PT_CUT_EACH}, + {63, "trim-after-job", PT_CUT_END}, + {0, "ptouch-chain", PT_CUT_CHAIN}, +}; +#define PT_NUM_FINISHINGS (sizeof pt_finishings / sizeof pt_finishings[0]) + +typedef struct { + pt_cut_policy policy; + uint8_t tape_mm; + bool first_label, page_active, pending, failed; +} pt_batch; + +static bool pt_finishing_value(int value, pt_cut_mode *mode) +{ + for (size_t i = 0; i < PT_NUM_FINISHINGS; i++) + if (pt_finishings[i].value && pt_finishings[i].value == value) { + *mode = pt_finishings[i].mode; + return true; + } + return false; +} +/* Read the original standard attributes: PAPPL's legacy mask loses 60/63 and + * the chain template. Omission follows the current synchronized printer default. */ +static bool pt_job_policy(pappl_job_t *job, pt_cut_policy *policy) +{ + ipp_attribute_t *values = papplJobGetAttribute(job, "finishings"); + ipp_attribute_t *collection = papplJobGetAttribute(job, "finishings-col"); + pt_cut_mode mode = PT_CUT_NONE; + if (values && collection) return false; + if (collection) { + if (ippGetValueTag(collection) == IPP_TAG_NOVALUE) { + *policy = pt_cut_policy_for(PT_CUT_NONE); + return true; + } + if (ippGetValueTag(collection) != IPP_TAG_BEGIN_COLLECTION || ippGetCount(collection) != 1) + return false; + ipp_t *col = ippGetCollection(collection, 0); + ipp_attribute_t *template = ippFindAttribute(col, "finishing-template", IPP_TAG_KEYWORD); + const char *name = template ? ippGetString(template, 0, NULL) : NULL; + if (!name || ippGetCount(template) != 1) return false; + for (size_t i = 0; i < PT_NUM_FINISHINGS; i++) + if (pt_finishings[i].name && !strcmp(pt_finishings[i].name, name)) { + *policy = pt_cut_policy_for(pt_finishings[i].mode); + return true; + } + return false; + } + if (values) { + if (ippGetValueTag(values) != IPP_TAG_ENUM || ippGetCount(values) < 1) return false; + for (int i = 0; i < ippGetCount(values); i++) { + pt_cut_mode selected; + if (!pt_finishing_value(ippGetInteger(values, i), &selected)) return false; + /* FIN: none is ignored alongside other finishings; 60 already + * includes the final cut, so adding 63 does not cut twice. */ + if (selected == PT_CUT_EACH || mode == PT_CUT_NONE) mode = selected; + } + } else { + ipp_t *attrs = papplPrinterGetDriverAttributes(papplJobGetPrinter(job)); + ipp_attribute_t *def = attrs ? ippFindAttribute(attrs, "finishings-default", IPP_TAG_ENUM) : NULL; + bool valid = def && ippGetCount(def) == 1 && pt_finishing_value(ippGetInteger(def, 0), &mode); + ippDelete(attrs); + if (!valid) return false; + } + *policy = pt_cut_policy_for(mode); + return true; +} -/* Render one planned op to the device (PRINT_PAGE is the raster, handled by rwriteline). */ -static bool pt_emit_op(pappl_device_t *d, const pt_op *op, uint8_t tape_mm) +static void pt_finishing_attrs(ipp_t *attrs) { - uint8_t buf[16]; - size_t n = 0; - switch (op->kind) { - case PT_OP_PRINTINFO: n = pt_cmd_printinfo(buf, tape_mm); break; - case PT_OP_SETMODE: n = pt_cmd_setmode(buf, op->arg); break; - case PT_OP_SETADVANCED: n = pt_cmd_setadvanced(buf, op->arg); break; - case PT_OP_FF: n = pt_cmd_ff(buf); break; - case PT_OP_EJECT: n = pt_cmd_eject(buf); break; - case PT_OP_PRINT_PAGE: return true; /* raster handled elsewhere */ + int values[PT_NUM_FINISHINGS]; + const char *templates[PT_NUM_FINISHINGS]; + ipp_t *collections[PT_NUM_FINISHINGS]; + size_t nvalues = 0, ntemplates = 0; + for (size_t i = 0; i < PT_NUM_FINISHINGS; i++) { + if (pt_finishings[i].value) values[nvalues++] = pt_finishings[i].value; + if (!pt_finishings[i].name) continue; + templates[ntemplates] = pt_finishings[i].name; + collections[ntemplates] = ippNew(); + ippAddString(collections[ntemplates], IPP_TAG_ZERO, IPP_TAG_KEYWORD, + "finishing-template", NULL, pt_finishings[i].name); + ntemplates++; } - return pt_dev_write(d, buf, n); + ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-supported", (int)nvalues, values); + ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-ready", (int)nvalues, values); + ippAddInteger(attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-default", pt_finishings[1].value); + ippAddString(attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "finishings-col-supported", NULL, "finishing-template"); + ippAddStrings(attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "finishing-template-supported", (int)ntemplates, NULL, templates); + ippAddCollections(attrs, IPP_TAG_PRINTER, "finishings-col-database", (int)ntemplates, (const ipp_t **)collections); + ippAddCollections(attrs, IPP_TAG_PRINTER, "finishings-col-ready", (int)ntemplates, (const ipp_t **)collections); + ippAddCollection(attrs, IPP_TAG_PRINTER, "finishings-col-default", collections[0]); + for (size_t i = 0; i < ntemplates; i++) ippDelete(collections[i]); } /* Defined below with status_cb; forward-declared for the #38 live read in r_startjob. */ static bool media_col_for(int tape_mm, int length_centimm, int dpi, pappl_media_col_t *out); -/* ---- cross-job chaining state (#41) ------------------------------------- - * "chain open" means the last label of a job was finished with FF instead of - * EJECT, so the tape is still at the head and the run is not finalized. The - * status callback and raster callbacks can run on different threads, so - * every access takes g_chain_lock. Entries are keyed by printer ID, - * never by pappl_printer_t *: a stored pointer can dangle when the printer is - * deleted, an ID cannot (papplSystemFindPrinter just returns NULL). - * The same table carries `media_confirmed` (#43), which needs exactly the same - * per-printer, cross-thread storage. */ -#define PT_CHAIN_MAX 8 - -static pthread_mutex_t g_chain_lock = PTHREAD_MUTEX_INITIALIZER; -static struct { int printer_id; bool open; bool media_confirmed; } g_chain[PT_CHAIN_MAX]; - -/* Slot for this printer id, optionally allocating one. Call with the lock held. */ -static int pt_chain_slot(int id, bool create) +/* The queue owns its state; PAPPL serializes jobs, while status may run on + * another thread. Never call PAPPL APIs with this mutex held. */ +typedef enum { PT_RUN_CLOSED, PT_RUN_OPEN, PT_RUN_UNCERTAIN } pt_run; +typedef struct { + pthread_mutex_t lock; + pt_run run; + bool media_confirmed; +} pt_printer_state; + +static pt_printer_state *pt_state(pappl_printer_t *printer) { - int free_slot = -1; - for (int i = 0; i < PT_CHAIN_MAX; i++) { - if (g_chain[i].printer_id == id) - return i; - if (free_slot < 0 && g_chain[i].printer_id == 0) - free_slot = i; - } - if (!create || free_slot < 0) - return -1; - g_chain[free_slot].printer_id = id; - g_chain[free_slot].open = false; - g_chain[free_slot].media_confirmed = false; - return free_slot; + pappl_pr_driver_data_t data; + papplPrinterGetDriverData(printer, &data); + return data.extension; } -/* Reserve state before accepting a no-cut job; never silently cut on exhaustion. */ -static bool pt_chain_reserve(pappl_printer_t *printer) +static pt_run pt_run_get(pappl_printer_t *printer) { - pthread_mutex_lock(&g_chain_lock); - bool ok = pt_chain_slot(papplPrinterGetID(printer), true) >= 0; - pthread_mutex_unlock(&g_chain_lock); - return ok; + pt_printer_state *state = pt_state(printer); + if (!state) return PT_RUN_UNCERTAIN; + pthread_mutex_lock(&state->lock); + pt_run run = state->run; + pthread_mutex_unlock(&state->lock); + return run; } -static void pt_chain_set(pappl_printer_t *printer, bool open) +static void pt_run_set(pappl_printer_t *printer, pt_run run) { - pthread_mutex_lock(&g_chain_lock); - int s = pt_chain_slot(papplPrinterGetID(printer), open); - if (s >= 0) { - g_chain[s].open = open; - } - pthread_mutex_unlock(&g_chain_lock); + pt_printer_state *state = pt_state(printer); + if (!state) return; + pthread_mutex_lock(&state->lock); + state->run = run; + pthread_mutex_unlock(&state->lock); } static bool pt_chain_get(pappl_printer_t *printer) { - pthread_mutex_lock(&g_chain_lock); - int s = pt_chain_slot(papplPrinterGetID(printer), false); - bool open = s >= 0 && g_chain[s].open; - pthread_mutex_unlock(&g_chain_lock); - return open; + return pt_run_get(printer) == PT_RUN_OPEN; } -/* #43: has a live status read ever confirmed the loaded tape for this printer? - * Until it has, media-default is still the safe 12 mm placeholder from - * pt_driver_cb, and any page built from it is a guess. */ -static void pt_set_media_confirmed(pappl_printer_t *printer, bool confirmed) +/* Cassette/offline observations may invalidate a healthy run, but cannot prove + * that an interrupted command buffer is safe to reuse. */ +static void pt_chain_set(pappl_printer_t *printer, bool open) { - pthread_mutex_lock(&g_chain_lock); - int s = pt_chain_slot(papplPrinterGetID(printer), confirmed); - if (s >= 0) - g_chain[s].media_confirmed = confirmed; - pthread_mutex_unlock(&g_chain_lock); + pt_printer_state *state = pt_state(printer); + if (!state) return; + pthread_mutex_lock(&state->lock); + if (state->run != PT_RUN_UNCERTAIN) + state->run = open ? PT_RUN_OPEN : PT_RUN_CLOSED; + pthread_mutex_unlock(&state->lock); } -static bool pt_media_confirmed(pappl_printer_t *printer) +static void pt_set_media_confirmed(pappl_printer_t *printer, bool confirmed) { - pthread_mutex_lock(&g_chain_lock); - int s = pt_chain_slot(papplPrinterGetID(printer), false); - bool confirmed = s >= 0 && g_chain[s].media_confirmed; - pthread_mutex_unlock(&g_chain_lock); - return confirmed; + pt_printer_state *state = pt_state(printer); + if (!state) return; + pthread_mutex_lock(&state->lock); + state->media_confirmed = confirmed; + pthread_mutex_unlock(&state->lock); } -static void pt_chain_drop(int id) +static void pt_delete_cb(pappl_printer_t *printer, pappl_pr_driver_data_t *data) { - pthread_mutex_lock(&g_chain_lock); - int s = pt_chain_slot(id, false); - if (s >= 0) - memset(&g_chain[s], 0, sizeof g_chain[s]); - pthread_mutex_unlock(&g_chain_lock); + (void)printer; + pt_printer_state *state = data->extension; + if (state) { + pthread_mutex_destroy(&state->lock); + free(state); + data->extension = NULL; + } } -/* Drop the entry when PAPPL deletes the printer (tidy-up only: the ID lookup is - * what makes deletion safe). */ -static void pt_delete_cb(pappl_printer_t *printer, pappl_pr_driver_data_t *data) +static bool pt_recovery_required(pappl_printer_t *printer) { - (void)data; - pt_chain_drop(papplPrinterGetID(printer)); + if (pt_run_get(printer) != PT_RUN_UNCERTAIN) return false; + papplPrinterSetReasons(printer, PAPPL_PREASON_OTHER, PAPPL_PREASON_NONE); + papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, + "Output uncertain: power-cycle the printer, then restart the server before resubmitting"); + return true; } -/* Start of job: init the printer and select raster transfer mode. - * flags = 0: PT-2730 is FLAG_NONE. Per-model dispatch (P700 rasterstart variant, - * packbits) is deferred until there is non-PT-2730 hardware to verify it. */ +/* Resolve policy and confirm tape; setup waits for the first valid label. */ static bool r_startjob(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) { + (void)o; pappl_printer_t *printer = papplJobGetPrinter(j); - - /* strict width guard (#19; fit policy is #35). PTOUCH_WIDTH_GUARD: default/strict. - * Any non-strict value (e.g. fit) is unimplemented -> fall back to strict, never accept. */ - const char *gpol = getenv("PTOUCH_WIDTH_GUARD"); - if (gpol && strcmp(gpol, "strict") != 0) - papplLogPrinter(printer, PAPPL_LOGLEVEL_WARN, - "PTOUCH_WIDTH_GUARD=%s not implemented; using strict (#35)", gpol); - - pappl_pr_driver_data_t dd; - papplPrinterGetDriverData(printer, &dd); /* fills dd with current driver data incl. media_ready */ - int dpi = o->header.HWResolution[0] ? (int)o->header.HWResolution[0] : dd.x_default; - - /* #38: a job queued while the printer was offline keeps a stale OFFLINE reason - * (PAPPL suppresses status_cb while a job holds the device), which the strict - * width guard would fault as "tape unknown". Re-read the live tape from the - * already-open device, but ONLY when: - * (a) this is our ptouch:// device - pt_usb_read_status casts the device data - * to our libusb struct, so calling it on socket:// is undefined behaviour; and - * (b) the cached state is suspect - a healthy printer keeps the fast path - * (a dead device can cost ~4s: 10 tries x (100ms sleep + 300ms timeout)). */ - bool live_ok = false; - pt_status live = {0}; - const char *duri = papplPrinterGetDeviceURI(printer); - pappl_preason_t pr0 = papplPrinterGetReasons(printer); - if (duri && !strncmp(duri, "ptouch://", 9) && - (pr0 & (PAPPL_PREASON_OFFLINE | PAPPL_PREASON_MEDIA_EMPTY))) { - pt_live_read lr = pt_read_live_tape(d, !pt_chain_get(printer)); - if (lr.parsed && lr.st.tape_px > 0) { - pappl_media_col_t mc; - if (media_col_for(lr.st.tape_mm, 10000, dpi, &mc)) { - live = lr.st; - live_ok = true; - papplPrinterSetReadyMedia(printer, 1, &mc); - papplPrinterSetReasons(printer, PAPPL_PREASON_NONE, - PAPPL_PREASON_OFFLINE | PAPPL_PREASON_MEDIA_EMPTY); - } - } else if (lr.parsed && lr.st.tape_mm == 0) { - papplPrinterSetReasons(printer, PAPPL_PREASON_MEDIA_EMPTY, PAPPL_PREASON_OFFLINE); - } - } - - /* #39: compare like for like in FULL-PAGE px. cupsWidth is the full media width on every - * CUPS version (cups/raster-stream.c: cupsWidth = media->width * xdpi / 2540); margins only - * populate ImageBox*. size_width is the nominal tape width, so this is the same space. */ - int loaded_px = (int)(dd.media_ready[0].size_width * dpi / 2540); - unsigned w = o->header.cupsWidth; - - /* reasons re-read AFTER the live block, which may have cleared them */ - bool unknown = (papplPrinterGetReasons(printer) & - (PAPPL_PREASON_MEDIA_EMPTY | PAPPL_PREASON_OFFLINE)) != 0; - if (live_ok) { /* live read wins over cached state (#38) */ - /* #43: compare like for like. live.tape_px is the IMAGEABLE width (128 for 24mm), - * but `w` is cupsWidth, which since #39 is the FULL nominal page width (170 for - * 24mm). Convert the live tape to the same nominal space instead of mixing them - - * the old assignment faulted correct jobs whenever this branch fired, and is the - * source of the "!= loaded tape 128 px" message reported in #43. */ - loaded_px = (int)((live.tape_mm * 100) * dpi / 2540); - unknown = false; - } - if (unknown || (int)w != loaded_px) { - papplJobSetReasons(j, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); - papplJobSetMessage(j, "width guard (strict): job width %u px != loaded tape %d px%s", - w, loaded_px, unknown ? " (tape unknown)" : ""); - papplLogJob(j, PAPPL_LOGLEVEL_ERROR, - "width guard (strict): job width %u px != loaded tape %d px%s", - w, loaded_px, unknown ? " (tape unknown)" : ""); - return false; /* fault the job; no raster has been written */ - } - - /* The finishing choice is already resolved by PAPPL, including defaults - * and finishings-col. No queue or environment setting can override it. */ - pt_cut_mode mode = (o->finishings & PAPPL_FINISHINGS_TRIM) ? PT_CUT_EACH : PT_CUT_NONE; - if (mode == PT_CUT_NONE && !pt_chain_reserve(printer)) { - papplJobSetMessage(j, "no-cut run cannot be tracked: printer state table is full"); + pt_cut_policy policy; + if (papplJobIsCanceled(j)) return false; + if (!pt_job_policy(j, &policy)) { + papplJobSetMessage(j, "Unsupported or conflicting finishing selection"); return false; } - - /* A previous none job may have completed while the queue was empty. - * Reopening its handle does not make an ESC @ reset safe. */ - uint8_t buf[8]; - if (!pt_chain_get(printer) && !pt_dev_write(d, buf, pt_cmd_init(buf))) - return false; - if (!pt_dev_write(d, buf, pt_cmd_rasterstart(buf, 0))) - return false; - - int n = papplJobGetCopies(j); - if (n < 1) n = 1; - if (n > PT_MAX_LABELS) { - papplJobSetReasons(j, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); - papplJobSetMessage(j, "batch too large: %d copies (max %d)", n, PT_MAX_LABELS); + const char *format = papplJobGetFormat(j); + if (format && (!strcmp(format, "image/pwg-raster") || !strcmp(format, "image/urf")) && + papplJobGetCopies(j) != 1) { + papplJobSetMessage(j, "Raster batches require copies=1; include each label in the document"); return false; } - int precut = 1; - const char *pc = getenv("PTOUCH_PRECUT"); - if (pc && !strcmp(pc, "0")) precut = 0; - - pt_batch *b = calloc(1, sizeof *b); - if (!b) return false; - /* live tape mm when the re-read succeeded; otherwise reverse-lookup from the - * width guard's loaded_px (Codex #1) */ - /* #39: size_width is the nominal tape width, so take mm from it directly; loaded_px is - * full-page px now and pt_mm_for_px() expects imageable px. */ - b->tape_mm = live_ok ? (uint8_t)live.tape_mm : (uint8_t)(dd.media_ready[0].size_width / 100); - b->n_ops = pt_plan_batch(n, b->tape_mm, mode, precut, false, b->ops, PT_OPS_CAP); - if (b->n_ops < 0) { /* cap exceeded (Codex #5) */ - free(b); - papplJobSetReasons(j, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); - papplJobSetMessage(j, "cutter plan overflow for %d copies", n); + if (pt_recovery_required(printer)) { + papplJobSetMessage(j, "Output uncertain: power-cycle printer, then restart server"); return false; } - b->index = 0; - papplJobSetData(j, b); + if (!pt_refresh_ready_media(j, d)) return false; + pappl_pr_driver_data_t dd; + papplPrinterGetDriverData(printer, &dd); + pt_batch *batch = calloc(1, sizeof *batch); + if (!batch) return false; + batch->policy = policy; + batch->tape_mm = (uint8_t)(dd.media_ready[0].size_width / 100); + batch->first_label = true; + papplJobSetData(j, batch); return true; } +static bool pt_wait_ready(pappl_job_t *j, pappl_device_t *d); + +/* Consume the one complete pending label only when the next valid label or + * normal job completion proves whether it is intermediate or final. */ +static bool pt_print_pending(pappl_job_t *job, pappl_device_t *device, bool final) +{ + pt_batch *batch = papplJobGetData(job); + uint8_t command[8]; + bool cut = final && batch->policy.cut_at_end; + if (papplJobIsCanceled(job)) return false; + if (cut && !pt_dev_write(device, command, pt_cmd_setadvanced(command, 8))) return false; + if (papplJobIsCanceled(job)) return false; + if (!pt_dev_write(device, command, cut ? pt_cmd_eject(command) : pt_cmd_ff(command)) || + !pt_wait_ready(job, device)) return false; + batch->pending = false; + pt_run_set(papplJobGetPrinter(job), cut ? PT_RUN_CLOSED : PT_RUN_OPEN); + return true; +} + +static bool pt_fail_job(pappl_job_t *job) +{ + pt_batch *batch = papplJobGetData(job); + if (batch) batch->failed = true; + return false; +} + +/* CUPS custom sizes can quantize a tape width by one 1/100 mm unit. */ +static bool pt_page_width_matches(const pappl_pr_options_t *o, const pt_geometry *g, int tape_mm) +{ + if (o->header.cupsWidth == (unsigned)g->nominal_px) return true; + int width = o->media.size_width; + return width >= tape_mm * 100 - 1 && width <= tape_mm * 100 + 1 && + o->header.cupsWidth == (unsigned)(width * 180 / 2540) && + o->header.cupsWidth >= (unsigned)g->right_px; +} + static bool r_startpage(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d, unsigned p) { - (void)o; (void)p; + (void)p; pt_batch *b = papplJobGetData(j); - if (!b) return true; - if (papplJobIsCanceled(j)) return false; - pt_chain_set(papplJobGetPrinter(j), true); - - for (int k = 0; k < b->n_ops; k++) { - if (b->ops[k].page_index != b->index) continue; - if (b->ops[k].kind == PT_OP_PRINT_PAGE) break; /* raster follows */ - if (!pt_emit_op(d, &b->ops[k], b->tape_mm)) return false; + if (!b || b->failed || b->page_active || papplJobIsCanceled(j)) return pt_fail_job(j); + pt_geometry g; + if (!pt_tape_geometry(b->tape_mm, &g) || o->header.HWResolution[0] != 180 || + o->header.HWResolution[1] != 180 || o->header.cupsBitsPerPixel != 1 || + !pt_page_width_matches(o, &g, b->tape_mm) || + o->header.cupsHeight < PT_ROLL_MIN_CENTIMM * 180 / 2540 || + o->header.cupsHeight > PT_ROLL_MAX_CENTIMM * 180 / 2540) { + papplJobSetReasons(j, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); + papplJobSetMessage(j, "Page geometry does not match loaded %d mm tape at 180 dpi", b->tape_mm); + return pt_fail_job(j); + } + int left = o->media.left_margin * 180 / 2540; + int span = (int)o->header.cupsWidth - (o->media.left_margin + o->media.right_margin) * 180 / 2540; + if (left < g.left_px || span <= 0 || left + span > g.right_px) { + papplJobSetMessage(j, "Selected margins extend beyond the physical printable window"); + return pt_fail_job(j); } -#if PT_LEADER_PX > 0 - /* Leading-edge quirk: feed PT_LEADER_PX blank cross-tape lines so leading - * content is not clipped. Adds to the cut length (Codex #4). Default 0. */ - { uint8_t blank[16] = {0}, buf[24]; - for (int i = 0; i < PT_LEADER_PX; i++) - if (!pt_dev_write(d, buf, pt_cmd_sendraster(buf, blank, 16))) return false; - papplDeviceFlush(d); } -#endif + pappl_printer_t *printer = papplJobGetPrinter(j); + /* Page setup is sent only after validating the actual page, never a stale + * default header. Keep a healthy preceding run if validation rejected it. */ + pt_run run = pt_run_get(printer); + if (run == PT_RUN_UNCERTAIN && b->first_label) return pt_fail_job(j); + if (b->pending && !pt_print_pending(j, d, false)) return pt_fail_job(j); + bool continuation = pt_chain_get(printer); + pt_run_set(printer, PT_RUN_UNCERTAIN); + uint8_t buf[8]; + if (b->first_label && + ((!continuation && !pt_dev_write(d, buf, pt_cmd_init(buf))) || + !pt_dev_write(d, buf, pt_cmd_rasterstart(buf, 0)))) + return pt_fail_job(j); + if (!pt_dev_write(d, buf, pt_cmd_printinfo(buf, b->tape_mm)) || + !pt_dev_write(d, buf, pt_cmd_setmode(buf, pt_cut_autocut(b->policy, b->first_label) ? 0x40 : 0)) || + !pt_dev_write(d, buf, pt_cmd_setadvanced(buf, 0))) + return pt_fail_job(j); + b->first_label = false; + b->page_active = true; return true; } @@ -383,29 +405,15 @@ static bool r_startpage(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d static bool r_writeline(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d, unsigned y, const unsigned char *l) { (void)y; - /* #39: with nominal media sizes the page raster is the FULL tape width (cupsWidth), - * and the printable area is a window inside it - cupsWidth is NOT reduced by margins on - * any CUPS version (see cups/raster-stream.c: cupsWidth = media->width * xdpi / 2540). - * Extract [left_px, left_px + win) and centre that in the 128-dot head. With zero - * margins this degenerates to the previous behaviour (start 0, win = cupsWidth). */ - int dpi = o->header.HWResolution[0] ? (int)o->header.HWResolution[0] : 180; - int width = (int)o->header.cupsWidth; - int left = o->media.left_margin * dpi / 2540; - int right = o->media.right_margin * dpi / 2540; - int win = width - left - right; - if (win > 128) win = 128; /* never exceed the head */ - if (win < 0) win = 0; - if (left + win > width) win = width - left; - if (win <= 0) { - papplLogJob(j, PAPPL_LOGLEVEL_ERROR, - "empty printable window (cupsWidth=%d left=%d right=%d)", width, left, right); - return false; - } - uint8_t packed[16]; - pt_pack_window(packed, l, left, win); - uint8_t buf[24]; - /* One checked transfer per line avoids bursts overrunning the printer buffer. */ - return pt_dev_write(d, buf, pt_cmd_sendraster(buf, packed, 16)); + pt_batch *batch = papplJobGetData(j); + if (!batch || batch->failed || !batch->page_active || papplJobIsCanceled(j)) return pt_fail_job(j); + pt_geometry g; + if (!pt_tape_geometry(batch->tape_mm, &g) || + !pt_page_width_matches(o, &g, batch->tape_mm)) + return pt_fail_job(j); + uint8_t packed[16], buf[24]; + pt_pack_window(packed, l, g.left_px, g.imageable_px); + return pt_dev_write(d, buf, pt_cmd_sendraster(buf, packed, sizeof packed)) || pt_fail_job(j); } /* Wait for receive-ready, not just print-completed. This is USB flow control, @@ -416,14 +424,14 @@ static long pt_ms_since(const struct timespec *t0); static bool pt_wait_ready(pappl_job_t *j, pappl_device_t *d) { const char *uri = papplPrinterGetDeviceURI(papplJobGetPrinter(j)); - if (!uri || strncmp(uri, "ptouch://", 9)) return true; + if (!pt_usb_is_uri(uri)) return true; uint8_t packet[32]; size_t used = 0; struct timespec start; clock_gettime(CLOCK_MONOTONIC, &start); while (pt_ms_since(&start) < PT_PRINT_READY_TIMEOUT_MS) { if (papplJobIsCanceled(j)) return false; - ssize_t n = pt_usb_read_status(d, packet + used, sizeof packet - used); + ssize_t n = pt_usb_read_status(d, uri, packet + used, sizeof packet - used); if (n < 0) { papplJobSetMessage(j, "printer status read failed after printing"); return false; @@ -451,68 +459,40 @@ static bool pt_wait_ready(pappl_job_t *j, pappl_device_t *d) static bool r_endpage(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d, unsigned p) { - (void)o; (void)p; - pt_batch *b = papplJobGetData(j); - if (!b) return true; - if (papplJobIsCanceled(j)) return false; - bool seen_print = false; - for (int k = 0; k < b->n_ops; k++) { - if (b->ops[k].page_index != b->index) continue; - if (seen_print) { - if (!pt_emit_op(d, &b->ops[k], b->tape_mm)) return false; - if (b->ops[k].kind == PT_OP_EJECT) - pt_chain_set(papplJobGetPrinter(j), false); - if (!pt_wait_ready(j, d)) return false; - b->index++; - return true; - } - if (b->ops[k].kind == PT_OP_PRINT_PAGE) seen_print = true; - } - return false; + (void)o; (void)d; (void)p; + pt_batch *batch = papplJobGetData(j); + if (!batch || batch->failed || !batch->page_active || papplJobIsCanceled(j)) return pt_fail_job(j); + batch->page_active = false; + batch->pending = true; + return true; } -/* Cleanup never invents a cut or replays an uncertain terminal command. */ +/* Abort cleanup never sends the pending label or replays a terminal attempt. */ static bool r_endjob(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) { - (void)o; (void)d; - free(papplJobGetData(j)); + (void)o; + pt_batch *batch = papplJobGetData(j); + if (!batch) return true; + bool normal = !batch->failed && !papplJobIsCanceled(j) && + papplJobGetState(j) < IPP_JSTATE_CANCELED; + bool result = true; + if (normal && batch->page_active) result = false; + else if (normal && batch->pending) result = pt_print_pending(j, d, true); + free(batch); papplJobSetData(j, NULL); - return true; -} - -/* Raw-print callback. Only needed because PAPPL's validate_driver reads a - * non-null driver_data.format as "supports raw printing" and rejects the driver - * without a printfile_cb. PT_RASTER_FORMAT is a filter target, not a format a - * client can usefully send, so raw data in it faults (#40). */ -static bool pt_printfile_cb(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) -{ - (void)o; (void)d; - papplJobSetReasons(j, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); - papplJobSetMessage(j, "raw '%s' data is not printable; send image/png", PT_RASTER_FORMAT); - papplLogJob(j, PAPPL_LOGLEVEL_ERROR, - "raw '%s' data is not printable; send image/png", PT_RASTER_FORMAT); - return false; + return result; } -/* Fill a media-col for a loaded tape width (mm) at a continuous length (1/100 mm). - * The cross-tape dimension is the tape's imageable px expressed in 1/100 mm - * (round-trips to exactly pt_tape_px). Returns false for an unknown/too-wide - * width. size_name is owned by the struct copy, so formatting it here is safe. */ +/* Publish nominal tape dimensions with pixel-aligned physical margins. */ static bool media_col_for(int tape_mm, int length_centimm, int dpi, pappl_media_col_t *out) { - memset(out, 0, sizeof(*out)); /* zero even on the false path so callers that ignore the return get a safe col */ - int px = pt_tape_px(tape_mm); - if (px <= 0 || px > 128) /* 128 = head width, the actual cross-tape limit */ - return false; - /* #39: advertise the NOMINAL tape width with real margins, so clients can match the - * tape they bought (a 24mm template vs 24mm tape). The printable area is the - * head-limited imageable width; the difference is unprintable edge, expressed as - * symmetric left/right margins. Requires CUPS >= 2.5, where PAPPL passes margins into - * cupsRasterInitHeader so cupsWidth is the PRINTABLE width (2400-296-296 -> 128px). */ - int printable = pt_px_to_centimm(px, dpi); + memset(out, 0, sizeof *out); + pt_geometry g; + if (dpi != 180 || !pt_tape_geometry(tape_mm, &g)) return false; out->size_width = tape_mm * 100; out->size_length = length_centimm; - out->left_margin = out->right_margin = (out->size_width - printable) / 2; + out->left_margin = g.left_margin_centimm; + out->right_margin = g.right_margin_centimm; pwgFormatSizeName(out->size_name, sizeof(out->size_name), NULL, NULL, out->size_width, length_centimm, "mm"); strncpy(out->source, "main-roll", sizeof(out->source) - 1); @@ -521,64 +501,129 @@ static bool media_col_for(int tape_mm, int length_centimm, int dpi, pappl_media_ return true; } -/* Read the loaded tape over USB and publish media-ready + printer reasons. - * Distinguishes unplugged (OFFLINE) from in-use (NULL open while present). */ -static bool status_cb(pappl_printer_t *printer) +/* PAPPL's generated database applies one printer-wide minimum to every + * size. Standard clients need the cassette's actual margins, including custom + * lengths, before rendering a page that the physical guard can accept. */ +static void pt_media_attrs(ipp_t *attrs) { - char model[64]; - parse_mdl(papplPrinterGetDeviceID(printer), model, sizeof model); - const pt_dev *dt = pt_lookup_name(model); - if (!dt) - dt = pt_lookup_dev(0x04f9, 0x2041); /* fallback PT-2730 */ - - if (!pt_usb_present(dt->vid, dt->pid)) { - /* unplugged: OFFLINE only; drop any stale MEDIA_EMPTY from a prior poll */ - papplPrinterSetReasons(printer, PAPPL_PREASON_OFFLINE, PAPPL_PREASON_MEDIA_EMPTY); - pt_chain_set(printer, false); /* #41: the tape left with the device */ - pt_set_media_confirmed(printer, false); /* #43: a power-cycle must re-confirm */ - return true; + ipp_t *database[PT_MAX_MEDIA_NAMES], *sizes[sizeof pt_tape_mm / sizeof pt_tape_mm[0]]; + int left[sizeof pt_tape_mm / sizeof pt_tape_mm[0]], right[sizeof pt_tape_mm / sizeof pt_tape_mm[0]]; + int ncols = 0, nwidths = 0; + const int presets = (int)(sizeof pt_preset_mm / sizeof pt_preset_mm[0]); + for (size_t i = 0; i < sizeof pt_tape_mm / sizeof pt_tape_mm[0]; i++) { + pt_geometry geometry; + if (!pt_tape_geometry(pt_tape_mm[i], &geometry)) continue; + left[nwidths] = geometry.left_margin_centimm; + right[nwidths] = geometry.right_margin_centimm; + sizes[nwidths] = ippNew(); + ippAddRange(sizes[nwidths], IPP_TAG_ZERO, "x-dimension", pt_tape_mm[i] * 100 - 1, pt_tape_mm[i] * 100 + 1); + ippAddRange(sizes[nwidths], IPP_TAG_ZERO, "y-dimension", PT_ROLL_MIN_CENTIMM, PT_ROLL_MAX_CENTIMM); + for (int k = 0; k <= presets; k++) { + pappl_media_col_t media; + media_col_for(pt_tape_mm[i], k < presets ? pt_preset_mm[k] * 100 : PT_ROLL_MIN_CENTIMM, 180, &media); + ipp_t *col = ippNew(), *size = k == presets ? sizes[nwidths] : ippNew(); + if (k < presets) { + ippAddInteger(size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "x-dimension", media.size_width); + ippAddInteger(size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "y-dimension", media.size_length); + ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "media-size-name", NULL, media.size_name); + } + ippAddCollection(col, IPP_TAG_ZERO, "media-size", size); + if (k < presets) ippDelete(size); + ippAddInteger(col, IPP_TAG_ZERO, IPP_TAG_INTEGER, "media-left-margin", media.left_margin); + ippAddInteger(col, IPP_TAG_ZERO, IPP_TAG_INTEGER, "media-right-margin", media.right_margin); + ippAddInteger(col, IPP_TAG_ZERO, IPP_TAG_INTEGER, "media-top-margin", 0); + ippAddInteger(col, IPP_TAG_ZERO, IPP_TAG_INTEGER, "media-bottom-margin", 0); + ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "media-source", NULL, media.source); + ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "media-type", NULL, media.type); + database[ncols++] = col; + } + nwidths++; } - - pappl_device_t *dev = papplPrinterOpenDevice(printer); - if (!dev) { - /* present but not openable: a job owns it; clear OFFLINE, leave media */ - papplPrinterSetReasons(printer, PAPPL_PREASON_NONE, PAPPL_PREASON_OFFLINE); - return true; + ippAddCollections(attrs, IPP_TAG_PRINTER, "media-col-database", ncols, (const ipp_t **)database); + ippAddCollections(attrs, IPP_TAG_PRINTER, "media-size-supported", nwidths, (const ipp_t **)sizes); + /* Publish ascending unique values so a generic minimum remains accurate. */ + int *margins[] = {left, right}; + const char *names[] = {"media-left-margin-supported", "media-right-margin-supported"}; + for (int side = 0; side < 2; side++) { + for (int i = 1; i < nwidths; i++) { + int value = margins[side][i], j = i; + while (j && margins[side][j - 1] > value) { margins[side][j] = margins[side][j - 1]; j--; } + margins[side][j] = value; + } + int n = 0; + for (int i = 0; i < nwidths; i++) + if (!n || margins[side][i] != margins[side][n - 1]) margins[side][n++] = margins[side][i]; + ippAddIntegers(attrs, IPP_TAG_PRINTER, IPP_TAG_INTEGER, names[side], n, margins[side]); } + for (int i = 0; i < ncols; i++) ippDelete(database[i]); + for (int i = 0; i < nwidths; i++) ippDelete(sizes[i]); +} - /* Keep cassette detection while omitting the reset for unfinished tape. */ - pt_live_read lr = pt_read_live_tape(dev, !pt_chain_get(printer)); - papplPrinterCloseDevice(printer); +#define PT_STATUS_REASONS (PAPPL_PREASON_OFFLINE | PAPPL_PREASON_MEDIA_EMPTY | PAPPL_PREASON_MEDIA_NEEDED | PAPPL_PREASON_OTHER) - pt_status st = lr.st; - if (lr.parsed && st.tape_px > 0) { - /* #41: a cassette swapped mid-chain must not inherit the flag - the tape at - * the head is gone, so treat the run as finished (the next job pre-cuts). */ - pappl_pr_driver_data_t dd; - papplPrinterGetDriverData(printer, &dd); - if ((int)(dd.media_ready[0].size_width / 100) != st.tape_mm) - pt_chain_set(printer, false); - pappl_media_col_t mc; - if (media_col_for(st.tape_mm, 10000, dt->dpi, &mc) && - papplPrinterSetReadyMedia(printer, 1, &mc)) { - papplPrinterSetReasons(printer, PAPPL_PREASON_NONE, - PAPPL_PREASON_OFFLINE | PAPPL_PREASON_MEDIA_EMPTY); - pt_set_media_confirmed(printer, true); /* #43 */ - } else { - papplPrinterSetReasons(printer, PAPPL_PREASON_NONE, PAPPL_PREASON_OFFLINE); +/* Idle polling and job-owned refresh publish the same clean tape/error result. */ +static bool pt_publish_tape(pappl_printer_t *printer, pt_live_read live) +{ + pappl_preason_t reasons; + if (!live.parsed) { + reasons = PAPPL_PREASON_OFFLINE; + } else if (live.st.error) { + reasons = PAPPL_PREASON_OTHER; + papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, + "Printer status error=0x%04x, tape=%d mm", live.st.error, live.st.tape_mm); + } else if (!live.st.tape_mm) { + reasons = PAPPL_PREASON_MEDIA_EMPTY; + pt_chain_set(printer, false); + } else { + pappl_media_col_t media; + pappl_pr_driver_data_t data; + papplPrinterGetDriverData(printer, &data); + if (media_col_for(live.st.tape_mm, 10000, data.x_default, &media) && + papplPrinterSetReadyMedia(printer, 1, &media)) { + if (data.media_ready[0].size_width != media.size_width) + pt_chain_set(printer, false); + pt_set_media_confirmed(printer, true); + papplPrinterSetReasons(printer, PAPPL_PREASON_NONE, PT_STATUS_REASONS); + return true; } - } else if (lr.parsed && st.tape_mm == 0) { - papplPrinterSetReasons(printer, PAPPL_PREASON_MEDIA_EMPTY, PAPPL_PREASON_OFFLINE); - pt_chain_set(printer, false); /* #41: no tape, nothing to finalize */ - pt_set_media_confirmed(printer, false); /* #43 */ - } else if (lr.n < 0) { - /* I/O error: device likely gone; OFFLINE, drop any stale MEDIA_EMPTY */ - papplPrinterSetReasons(printer, PAPPL_PREASON_OFFLINE, PAPPL_PREASON_MEDIA_EMPTY); - pt_chain_set(printer, false); /* #41 */ - pt_set_media_confirmed(printer, false); /* #43 */ - } else { /* 0 / short / garbled: transient, keep last-known media */ - papplPrinterSetReasons(printer, PAPPL_PREASON_NONE, PAPPL_PREASON_OFFLINE); + reasons = PAPPL_PREASON_MEDIA_NEEDED | PAPPL_PREASON_OTHER; + papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, + "Loaded tape width %d mm cannot be published", live.st.tape_mm); + } + pt_set_media_confirmed(printer, false); + papplPrinterSetReasons(printer, reasons, PT_STATUS_REASONS & ~reasons); + return false; +} + +static bool status_cb(pappl_printer_t *printer) +{ + /* The job owns live status and may have an unfinished command in flight. */ + if (papplPrinterGetState(printer) == IPP_PSTATE_PROCESSING) return true; + if (pt_recovery_required(printer)) return true; + const char *uri = papplPrinterGetDeviceURI(printer); + if (!pt_usb_is_uri(uri)) return true; + const pt_dev *model = pt_lookup_name("PT-2730"); + ssize_t count = pt_usb_count(model->vid, model->pid); + if (count != 1) { + papplPrinterSetReasons(printer, PAPPL_PREASON_OFFLINE, PT_STATUS_REASONS & ~PAPPL_PREASON_OFFLINE); + pt_set_media_confirmed(printer, false); + if (!count) pt_chain_set(printer, false); + if (count > 1) papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, "Multiple PT-2730 devices match; selection is ambiguous"); + return true; + } + pappl_device_t *device = papplPrinterOpenDevice(printer); + if (!device) { + /* Scheduling may have claimed the device after the idle check. */ + if (papplPrinterGetState(printer) == IPP_PSTATE_PROCESSING) return true; + papplPrinterSetReasons(printer, PAPPL_PREASON_OFFLINE, PT_STATUS_REASONS & ~PAPPL_PREASON_OFFLINE); + pt_set_media_confirmed(printer, false); + papplLogPrinter(printer, PAPPL_LOGLEVEL_ERROR, "PT-2730 is present but cannot be opened or claimed"); + return true; } + /* Status requests do not initialize or finalize the printer. */ + pt_live_read live = pt_read_live_tape(device, uri); + papplPrinterCloseDevice(printer); + pt_publish_tape(printer, live); return true; } @@ -600,74 +645,23 @@ static long pt_ms_since(const struct timespec *t0) bool pt_refresh_ready_media(pappl_job_t *job, pappl_device_t *device) { pappl_printer_t *printer = papplJobGetPrinter(job); - const char *duri = papplPrinterGetDeviceURI(printer); - - /* pt_usb_read_status casts the device data to our libusb struct, so a - * socket:// sink must never reach it (the same gate as #38 and #40). */ - if (!duri || strncmp(duri, "ptouch://", 9)) - return true; - /* An idle unfinished run can outlive its cassette. Query without ESC @ - * before building geometry; a confirmed closed run keeps the fast path. */ - bool open = pt_chain_get(printer); - if (!open && pt_media_confirmed(printer) && - !(papplPrinterGetReasons(printer) & (PAPPL_PREASON_OFFLINE | PAPPL_PREASON_MEDIA_EMPTY))) - return true; - - pappl_pr_driver_data_t dd; - papplPrinterGetDriverData(printer, &dd); - struct timespec t0; - clock_gettime(CLOCK_MONOTONIC, &t0); - - const char *fail = "tape not readable yet"; /* reason if the barrier gives up */ - for (;;) { - pt_live_read lr = pt_read_live_tape(device, !open); - if (lr.parsed && lr.st.tape_px > 0) { - pappl_media_col_t mc; - if (media_col_for(lr.st.tape_mm, 10000, dd.x_default, &mc) && - papplPrinterSetReadyMedia(printer, 1, &mc)) { - /* Only now is the state trustworthy: both the read and the publish - * succeeded, so the reasons may be cleared. */ - papplPrinterSetReasons(printer, PAPPL_PREASON_NONE, - PAPPL_PREASON_OFFLINE | PAPPL_PREASON_MEDIA_EMPTY); - if ((int)(dd.media_ready[0].size_width / 100) != lr.st.tape_mm) - pt_chain_set(printer, false); - pt_set_media_confirmed(printer, true); - /* papplPrinterSetReadyMedia reaches media_default only when the ready - * entry's source matches the default's ("main-roll" in both, from - * media_col_for), and papplJobCreatePrintOptions builds the page from - * media_default. Log what actually landed there so a future divergence - * is visible instead of silently resurrecting this bug. */ - papplPrinterGetDriverData(printer, &dd); - papplLogJob(job, PAPPL_LOGLEVEL_DEBUG, - "#43 media refresh: default now %d centimm (tape %d mm)", - dd.media_default.size_width, lr.st.tape_mm); - return true; - } - fail = "loaded tape is not supported"; /* re-reading returns the same width */ - break; - } - if (lr.parsed && lr.st.tape_mm == 0) { /* answered, cassette empty */ - papplPrinterSetReasons(printer, PAPPL_PREASON_MEDIA_EMPTY, PAPPL_PREASON_OFFLINE); - pt_chain_set(printer, false); - pt_set_media_confirmed(printer, false); - fail = "no tape loaded"; - break; - } - if (pt_ms_since(&t0) >= PT_MEDIA_REFRESH_MS) - break; - papplLogJob(job, PAPPL_LOGLEVEL_DEBUG, - "#43 media refresh: no status after %ld ms, retrying", pt_ms_since(&t0)); - struct timespec w = {0, PT_MEDIA_RETRY_NS}; - nanosleep(&w, NULL); + if (pt_recovery_required(printer)) { + papplJobSetMessage(job, "Output uncertain: power-cycle printer, then restart server"); + return false; } - - /* Do NOT fall through and build the page: papplJobCreatePrintOptions would - * snapshot the stale default, and r_startjob's live read can then only fault the - * finished header, not repair it. Failing loudly beats printing wrong geometry. */ + const char *uri = papplPrinterGetDeviceURI(printer); + if (!pt_usb_is_uri(uri)) return true; + struct timespec start; + clock_gettime(CLOCK_MONOTONIC, &start); + do { + pt_live_read live = pt_read_live_tape(device, uri); + if (pt_publish_tape(printer, live)) return true; + if (live.parsed || live.n != 0 || papplJobIsCanceled(job)) break; + struct timespec delay = {0, PT_MEDIA_RETRY_NS}; + nanosleep(&delay, NULL); + } while (pt_ms_since(&start) < PT_MEDIA_REFRESH_MS); papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); - papplJobSetMessage(job, "media refresh failed: %s", fail); - papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "#43 media refresh failed after %ld ms: %s", - pt_ms_since(&t0), fail); + papplJobSetMessage(job, "Live tape status is not clean, readable, and supported"); return false; } @@ -675,7 +669,21 @@ bool pt_driver_cb(pappl_system_t *system, const char *driver_name, const char *d const char *device_id, pappl_pr_driver_data_t *data, ipp_t **driver_attrs, void *cbdata) { (void)system; (void)driver_name; (void)device_uri; (void)device_id; - (void)driver_attrs; (void)cbdata; + (void)cbdata; + + char model[64]; + parse_mdl(device_id, model, sizeof model); + const pt_dev *dev = pt_lookup_name(model); + if (!dev || strcmp(dev->name, "PT-2730")) { + papplLog(system, PAPPL_LOGLEVEL_ERROR, + "Unsupported printer identity '%s'; only PT-2730 is print-supported", model); + return false; + } + + pt_printer_state *state = calloc(1, sizeof *state); + if (!state) return false; + if (pthread_mutex_init(&state->lock, NULL)) { free(state); return false; } + data->extension = state; data->rstartjob_cb = r_startjob; data->rstartpage_cb = r_startpage; @@ -683,19 +691,11 @@ bool pt_driver_cb(pappl_system_t *system, const char *driver_name, const char *d data->rendpage_cb = r_endpage; data->rendjob_cb = r_endjob; data->status_cb = status_cb; - data->delete_cb = pt_delete_cb; /* #41: forget this printer's chain state */ - /* #40: our own raster destination type, so main.c's image/png filter is - * chosen ahead of PAPPL's built-in one. printfile_cb is mandatory alongside - * `format`; it only faults (see pt_printfile_cb). Side effect: - * PT_RASTER_FORMAT appears in document-format-supported. */ - data->format = PT_RASTER_FORMAT; - data->printfile_cb = pt_printfile_cb; + data->delete_cb = pt_delete_cb; + /* PNG is both a normal public format and the owned filter destination. */ + data->format = "image/png"; + data->printfile_cb = pt_png_printfile; - char model[64]; - parse_mdl(device_id, model, sizeof model); - const pt_dev *dev = pt_lookup_name(model); - if (!dev) - dev = pt_lookup_dev(0x04f9, 0x2041); /* fallback PT-2730 */ int dpi = dev->dpi; int max_px = dev->max_px; @@ -710,18 +710,15 @@ bool pt_driver_cb(pappl_system_t *system, const char *driver_name, const char *d data->raster_types = PAPPL_PWG_RASTER_TYPE_BLACK_1 | PAPPL_PWG_RASTER_TYPE_BLACK_8; data->force_raster_type = PAPPL_PWG_RASTER_TYPE_BLACK_1; data->finishings = PAPPL_FINISHINGS_TRIM; + if (!*driver_attrs) *driver_attrs = ippNew(); + pt_finishing_attrs(*driver_attrs); data->num_resolution = 1; data->x_resolution[0] = data->y_resolution[0] = dpi; data->x_default = data->y_default = dpi; - /* 0/0 margins keep the imageable px exact: a nonzero margin would shrink - * media->width-2*margin and perturb cupsWidth (Codex #4). */ - /* #39: media-*-margin-supported. One printer-wide value; status_cb keeps it on the - * loaded tape's margin. Seeded from the safe default (12mm) before the first status read. */ - { - int dpx = pt_tape_px(12); - data->left_right = dpx > 0 ? (1200 - pt_px_to_centimm(dpx, dpi)) / 2 : 0; - } + /* One lower bound must admit every advertised cassette. Per-tape margins + * still constrain the actual physical window. */ + data->left_right = 53; data->bottom_top = 0; data->num_source = 1; data->source[0] = "main-roll"; @@ -768,5 +765,6 @@ bool pt_driver_cb(pappl_system_t *system, const char *driver_name, const char *d media_col_for(12, 10000, dpi, &mc); data->media_default = mc; data->media_ready[0] = mc; + pt_media_attrs(*driver_attrs); return true; } diff --git a/src/driver.h b/src/driver.h index de26576..149b6be 100644 --- a/src/driver.h +++ b/src/driver.h @@ -3,25 +3,13 @@ #include -/* Private raster destination type (#40). Job processing looks for a filter to - * driver_data.format BEFORE one to image/pwg-raster, so giving the driver its - * own destination type is what lets pt_filter_png replace PAPPL's built-in PNG - * filter (re-registering image/png -> image/pwg-raster is silently ignored). - * It is a filter target only, never a printable format: raw jobs in this type - * are faulted. */ -#define PT_RASTER_FORMAT "image/vnd.ptouch-raster" - /* Roll length range, in 1/100 mm, as advertised in media-col. */ #define PT_ROLL_MIN_CENTIMM 500 /* 5 mm */ #define PT_ROLL_MAX_CENTIMM 30000 /* 300 mm */ -/* #43: make media-default describe the tape that is actually loaded, before the - * caller builds the page geometry from it. A no-op unless the printer is on our - * ptouch:// scheme and its media is suspect (never confirmed by a live read, or - * the printer is offline/media-empty), so healthy jobs pay nothing. Returns false - * when suspect media could not be resolved within the barrier's budget: the job - * has already been faulted and the caller must not print. Call it with the - * device open and owned by the job. */ +/* Confirm clean live tape before geometry is constructed, using a bounded + * status query on the job-owned USB handle. Non-USB test transports retain + * configured media. Uncertain output blocks every transport until recovery. */ bool pt_refresh_ready_media(pappl_job_t *job, pappl_device_t *device); /* Fill driver_data for a P-touch printer (PAPPL pappl_pr_driver_cb_t). */ diff --git a/src/filter_png.c b/src/filter_png.c index a490b7c..20a616b 100644 --- a/src/filter_png.c +++ b/src/filter_png.c @@ -8,6 +8,7 @@ #include #include "filter_png.h" #include "driver.h" +#include "tables.h" /* Clients are documented to send 180 dpi (README); a PNG with no pHYs chunk is * read as such so its pixel height still means a real physical length. */ @@ -78,22 +79,119 @@ static bool pt_pin_orientation(pappl_pr_options_t *o, int width, int height) o->orientation_requested == IPP_ORIENT_REVERSE_LANDSCAPE; } +/* Submitted dimensions or a resolved size name suppress the PNG convenience. */ +static bool pt_explicit_length(pappl_job_t *job) +{ + ipp_attribute_t *attr = papplJobGetAttribute(job, "media-col"); + if (attr && ippGetValueTag(attr) == IPP_TAG_BEGIN_COLLECTION) { + ipp_t *col = ippGetCollection(attr, 0); + ipp_attribute_t *size_name = ippFindAttribute(col, "media-size-name", IPP_TAG_ZERO); + const char *name = size_name ? ippGetString(size_name, 0, NULL) : NULL; + pwg_media_t *media = name ? pwgMediaForPWG(name) : NULL; + /* PAPPL prefers a collection's size name over its dimensions. */ + if (media && media->length > 0) + return true; + ipp_attribute_t *size = ippFindAttribute(col, "media-size", IPP_TAG_BEGIN_COLLECTION); + if (size && ippFindAttribute(ippGetCollection(size, 0), "y-dimension", IPP_TAG_INTEGER)) + return true; + } + attr = papplJobGetAttribute(job, "media"); + const char *name = attr ? ippGetString(attr, 0, NULL) : NULL; + pwg_media_t *media = name ? pwgMediaForPWG(name) : NULL; + return media && media->length > 0; +} + +/* Preflight the oriented physical content before PAPPL's centered placement can + * clip it. A full tape canvas may include blank physical margins; compact only + * those verified margins, keeping the same orientation, DPI, and content. */ +static bool pt_preflight_png(pappl_job_t *job, pappl_pr_options_t *o, + unsigned char *pixels, int *width, int *height, + int depth, int ppi, bool landscape) +{ + if (o->print_scaling != PAPPL_SCALING_NONE) + return true; /* Explicit standard scaling remains PAPPL's responsibility. */ + + pt_geometry g; + if (!pt_tape_geometry((o->media.size_width + 50) / 100, &g)) + return false; + int cross = landscape ? *height : *width; + int along = landscape ? *width : *height; + int cross_px = (int)((int64_t)cross * PT_DEFAULT_PPI / ppi); + int along_px = (int)((int64_t)along * PT_DEFAULT_PPI / ppi); + int available = (int)o->header.cupsHeight - + (o->media.top_margin + o->media.bottom_margin) * PT_DEFAULT_PPI / 2540; + int printable = (int)o->header.cupsWidth - + (o->media.left_margin + o->media.right_margin) * PT_DEFAULT_PPI / 2540; + if (along_px < 1 || cross_px < 1 || along_px > available) { + papplJobSetMessage(job, "PNG content needs %d along-tape dots; page allows %d", along_px, available); + goto reject; + } + if (cross_px <= g.imageable_px && cross_px <= printable) + return true; + if (cross_px != g.nominal_px) { + papplJobSetMessage(job, "PNG content is %d dots across tape; printable width is %d", cross_px, + printable < g.imageable_px ? printable : g.imageable_px); + goto reject; + } + + bool reverse = o->orientation_requested == IPP_ORIENT_REVERSE_PORTRAIT || + o->orientation_requested == IPP_ORIENT_REVERSE_LANDSCAPE; + int first_dot = reverse ? g.nominal_px - g.right_px : g.left_px; + int last_dot = first_dot + g.imageable_px; + int first = (int)(((int64_t)first_dot * ppi + PT_DEFAULT_PPI - 1) / PT_DEFAULT_PPI); + int last = (int)((int64_t)last_dot * ppi / PT_DEFAULT_PPI); + if (first >= last || (int64_t)(last - first) * PT_DEFAULT_PPI / ppi > printable) { + papplJobSetMessage(job, "PNG content does not fit the selected media margins"); + goto reject; + } + for (int y = 0; y < *height; y++) { + for (int x = 0; x < *width; x++) { + int pos = landscape ? y : x; + if (pos >= first && pos < last) + continue; + for (int c = 0; c < depth; c++) { + if (pixels[((size_t)y * *width + x) * depth + c] != 255) { + papplJobSetMessage(job, "PNG has nonblank content outside the physical printable window"); + goto reject; + } + } + } + } + if (landscape) { + memmove(pixels, pixels + (size_t)first * *width * depth, + (size_t)(last - first) * *width * depth); + *height = last - first; + } else { + for (int y = 0; y < *height; y++) + memmove(pixels + (size_t)y * (last - first) * depth, + pixels + ((size_t)y * *width + first) * depth, + (size_t)(last - first) * depth); + *width = last - first; + } + return true; + +reject: + papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); + papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "%s", papplJobGetMessage(job)); + return false; +} + bool pt_filter_png(pappl_job_t *job, pappl_device_t *device, void *data) { const char *filename; FILE *fp; - pappl_pr_options_t *options = NULL; + png_structp pp = NULL; png_infop info = NULL; - png_bytep *rows = NULL; + png_color_16 bg; int i, color_type, width, height, depth, xdpi, ydpi, ppi; int along_tape_px, length_centimm; bool landscape; int max_width = 0, max_height = 0; size_t max_size; - unsigned char *pixels = NULL; - bool ret = false; + + volatile bool ret = false; (void)data; @@ -101,12 +199,30 @@ bool pt_filter_png(pappl_job_t *job, pappl_device_t *device, void *data) &max_width, &max_height); filename = papplJobGetFilename(job); + if (!filename) { + papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_FORMAT_ERROR, PAPPL_JREASON_NONE); + papplJobSetMessage(job, "PNG job has no document file"); + return false; + } if ((fp = fopen(filename, "rb")) == NULL) { papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unable to open PNG file '%s': %s", filename, strerror(errno)); return false; } + /* Mutable cleanup ownership lives outside the setjmp stack frame, so a + * libpng longjmp during rows/IEND leaves every allocation well defined. */ + struct { + pappl_pr_options_t *options; + png_bytep *rows; + unsigned char *pixels; + } *storage = calloc(1, sizeof *storage); + if (!storage) { + fclose(fp); + papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); + return false; + } + if ((pp = png_create_read_struct(PNG_LIBPNG_VER_STRING, (png_voidp)job, png_error_func, png_warning_func)) == NULL || (info = png_create_info_struct(pp)) == NULL) { @@ -170,11 +286,15 @@ bool pt_filter_png(pappl_job_t *job, pappl_device_t *device, void *data) /* #40: the label is as long as the image is, not as long as the media * default. Do this before any decode work so an over-length job faults * cheaply and before a single byte reaches the device. */ - options = papplJobCreatePrintOptions(job, 1, depth == 3); - landscape = pt_pin_orientation(options, width, height); + storage->options = papplJobCreatePrintOptions(job, 1, depth == 3); + if (!storage->options) + goto finish_png; + landscape = pt_pin_orientation(storage->options, width, height); along_tape_px = landscape ? width : height; - if ((length_centimm = pt_label_length(along_tape_px, ppi)) < 0) { + bool explicit_length = pt_explicit_length(job); + length_centimm = explicit_length ? storage->options->media.size_length : pt_label_length(along_tape_px, ppi); + if (length_centimm < 0) { papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); papplJobSetMessage(job, "label too long: %d px at %d dpi needs %d.%02d mm (max %d mm)", along_tape_px, ppi, along_tape_px * 2540 / ppi / 100, @@ -186,16 +306,17 @@ bool pt_filter_png(pappl_job_t *job, pappl_device_t *device, void *data) goto finish_png; } - pt_set_page_length(options, length_centimm); + if (!explicit_length) + pt_set_page_length(storage->options, length_centimm); papplLogJob(job, PAPPL_LOGLEVEL_INFO, "%s image, %d px along the tape at %d dpi -> media length %d (1/100 mm), " "header.cupsHeight=%u PWG_ImageBox=[%u %u %u %u]", landscape ? "landscape" : "portrait", along_tape_px, ppi, length_centimm, - options->header.cupsHeight, - options->header.cupsInteger[CUPS_RASTER_PWG_ImageBoxLeft], - options->header.cupsInteger[CUPS_RASTER_PWG_ImageBoxTop], - options->header.cupsInteger[CUPS_RASTER_PWG_ImageBoxRight], - options->header.cupsInteger[CUPS_RASTER_PWG_ImageBoxBottom]); + storage->options->header.cupsHeight, + storage->options->header.cupsInteger[CUPS_RASTER_PWG_ImageBoxLeft], + storage->options->header.cupsInteger[CUPS_RASTER_PWG_ImageBoxTop], + storage->options->header.cupsInteger[CUPS_RASTER_PWG_ImageBoxRight], + storage->options->header.cupsInteger[CUPS_RASTER_PWG_ImageBoxBottom]); /* Set decoding options... */ if (png_get_valid(pp, info, PNG_INFO_tRNS)) { @@ -229,11 +350,12 @@ bool pt_filter_png(pappl_job_t *job, pappl_device_t *device, void *data) png_set_palette_to_rgb(pp); /* Remove alpha by compositing over white... */ - bg.red = bg.green = bg.blue = 65535; + memset(&bg, 0, sizeof(bg)); + bg.gray = bg.red = bg.green = bg.blue = 65535; png_set_background(pp, &bg, PNG_BACKGROUND_GAMMA_SCREEN, 0, 1); - if ((pixels = calloc(1, (size_t)width * (size_t)height * (size_t)depth)) == NULL || - (rows = calloc((size_t)height, sizeof(png_bytep))) == NULL) { + if ((storage->pixels = calloc(1, (size_t)width * (size_t)height * (size_t)depth)) == NULL || + (storage->rows = calloc((size_t)height, sizeof(png_bytep))) == NULL) { papplLogJob(job, PAPPL_LOGLEVEL_ERROR, "Unable to allocate memory for PNG image: %s", strerror(errno)); papplJobSetReasons(job, PAPPL_JREASON_DOCUMENT_UNPRINTABLE_ERROR, PAPPL_JREASON_NONE); @@ -241,22 +363,35 @@ bool pt_filter_png(pappl_job_t *job, pappl_device_t *device, void *data) } for (i = 0; i < height; i++) - rows[i] = pixels + (size_t)i * (size_t)width * (size_t)depth; + storage->rows[i] = storage->pixels + (size_t)i * (size_t)width * (size_t)depth; for (i = png_set_interlace_handling(pp); i > 0; i--) - png_read_rows(pp, rows, NULL, (png_uint_32)height); + png_read_rows(pp, storage->rows, NULL, (png_uint_32)height); + + png_read_end(pp, info); /* Validate the complete PNG before output. */ + + if (!pt_preflight_png(job, storage->options, storage->pixels, &width, &height, depth, ppi, landscape)) + goto finish_png; /* Drives the whole raster lifecycle through the driver's callbacks (width * guard, cutter, offline handling all still apply). */ - ret = papplJobFilterImage(job, device, options, pixels, width, height, depth, ppi, false); + ret = papplJobFilterImage(job, device, storage->options, storage->pixels, width, height, depth, ppi, false); finish_png: png_destroy_read_struct(&pp, &info, NULL); fclose(fp); - papplJobDeletePrintOptions(options); - free(pixels); - free(rows); + papplJobDeletePrintOptions(storage->options); + free(storage->pixels); + free(storage->rows); + free(storage); return ret; } + +/* Raw PNG and the registered PNG filter share exactly one decoding/copy path. */ +bool pt_png_printfile(pappl_job_t *job, pappl_pr_options_t *options, pappl_device_t *device) +{ + (void)options; + return pt_filter_png(job, device, NULL); +} diff --git a/src/filter_png.h b/src/filter_png.h index 10325f1..d684abd 100644 --- a/src/filter_png.h +++ b/src/filter_png.h @@ -3,9 +3,9 @@ #include -/* Document filter for image/png -> PT_RASTER_FORMAT (pappl_mime_filter_cb_t). - * Same decode as PAPPL's built-in PNG filter, but the page length comes from the - * image's own physical size instead of the media default (#40). */ +/* Standard image/png -> image/png route with explicit-media precedence and + * source-content preflight. PAPPL supplies placement and raster callbacks. */ bool pt_filter_png(pappl_job_t *job, pappl_device_t *device, void *data); +bool pt_png_printfile(pappl_job_t *job, pappl_pr_options_t *options, pappl_device_t *device); #endif /* PT_FILTER_PNG_H */ diff --git a/src/main.c b/src/main.c index 1772219..fa2fae5 100644 --- a/src/main.c +++ b/src/main.c @@ -27,28 +27,33 @@ static bool probe_print_cb(const char *info, const char *uri, const char *id, vo return false; /* continue listing */ } +static void probe_error_cb(const char *message, void *data) +{ + (void)data; + fprintf(stderr, "probe: %s\n", message); +} + static int probe_cb(const char *base, int num_options, cups_option_t *options, int num_files, char **files, void *data) { (void)base; (void)num_options; (void)options; (void)num_files; (void)files; (void)data; pt_usb_register(); printf("probing ptouch:// (CUSTOM_LOCAL only)...\n"); - papplDeviceList(PAPPL_DEVTYPE_CUSTOM_LOCAL, probe_print_cb, NULL, NULL, NULL); + papplDeviceList(PAPPL_DEVTYPE_CUSTOM_LOCAL, probe_print_cb, NULL, probe_error_cb, NULL); printf("probe done\n"); printf("PRESENT 04f9:2041 = %s\n", pt_usb_present(0x04f9, 0x2041) ? "yes" : "no"); return 0; } -/* Capture the first ptouch:// device for in-process create. */ -typedef struct { char uri[256]; char id[256]; bool found; } pt_found; -static bool grab_cb(const char *info, const char *uri, const char *id, void *data) +static void discovery_error_cb(const char *message, void *data) +{ + papplLog(data, PAPPL_LOGLEVEL_ERROR, "%s", message); +} + +static bool discovery_cb(const char *info, const char *uri, const char *id, void *data) { - (void)info; - pt_found *f = data; - strncpy(f->uri, uri, sizeof f->uri - 1); - strncpy(f->id, id ? id : "", sizeof f->id - 1); - f->found = true; - return true; /* stop at the first */ + (void)info; (void)uri; (void)id; (void)data; + return false; } static pappl_system_t *system_cb(int num_options, cups_option_t *options, void *data) @@ -57,19 +62,17 @@ static pappl_system_t *system_cb(int num_options, cups_option_t *options, void * pappl_system_t *sys = papplSystemCreate( PAPPL_SOPTIONS_MULTI_QUEUE | PAPPL_SOPTIONS_WEB_INTERFACE, "ptouch", 8000, "_print,_universal", - NULL, "/tmp/ptouch.log", PAPPL_LOGLEVEL_DEBUG, NULL, false); + NULL, "-", PAPPL_LOGLEVEL_INFO, NULL, false); if (!sys) return NULL; + /* PAPPL 1.4.11 localizes its optional footer without checking for NULL. */ + papplSystemSetFooterHTML(sys, ""); papplSystemAddListeners(sys, NULL); pt_usb_register(); /* register the scheme BEFORE any list/open */ papplSystemSetPrinterDrivers(sys, NDRIVERS, g_drivers, NULL /*autoadd*/, NULL, pt_driver_cb, NULL); - /* #40: take over image/png so the label length follows the image. Must run - * before papplSystemRun, and must target our own PT_RASTER_FORMAT: PAPPL - * already has image/png -> image/pwg-raster and matches filters on src+dst - * only, so re-registering that pair would be silently dropped. */ - papplSystemAddMIMEFilter(sys, "image/png", PT_RASTER_FORMAT, pt_filter_png, NULL); + papplSystemAddMIMEFilter(sys, "image/png", "image/png", pt_filter_png, NULL); /* Test override: create the printer on an explicit URI (e.g. a socket sink) * instead of enumerating ptouch://. Lets the geometry gate run against @@ -85,38 +88,19 @@ static pappl_system_t *system_cb(int num_options, cups_option_t *options, void * return sys; } - /* In-process create from OUR scheme only (never the built-in usb:// hang). */ - pt_found found = {0}; - papplDeviceList(PAPPL_DEVTYPE_CUSTOM_LOCAL, grab_cb, &found, NULL, NULL); - if (found.found) { - pappl_printer_t *p = papplPrinterCreate(sys, 0, "ptouch", "brother_ptouch", - found.id, found.uri); - papplLog(sys, p ? PAPPL_LOGLEVEL_INFO : PAPPL_LOGLEVEL_ERROR, - "%s printer for %s", p ? "Created" : "FAILED to create", found.uri); - } else { - /* No device attached: still create the printer so clients see a real - * queue reporting `offline` instead of client-error-not-found, and so a - * power-cycle recovers without restarting (status_cb clears OFFLINE on - * the next attribute query). #38 */ - const char *model = getenv("PTOUCH_MODEL"); - if (!model || !*model) - model = "PT-2730"; - if (!pt_lookup_name(model)) { - papplLog(sys, PAPPL_LOGLEVEL_WARN, - "PTOUCH_MODEL=%s is not a known model; using PT-2730", model); - model = "PT-2730"; - } - char uri[256], id[256]; - snprintf(uri, sizeof uri, "ptouch://Brother/%s", model); - snprintf(id, sizeof id, "MFG:Brother;MDL:%s;CMD:PT-CBP;", model); - pappl_printer_t *p = papplPrinterCreate(sys, 0, "ptouch", "brother_ptouch", id, uri); - if (p) /* mark offline NOW: without it reasons are `none`, the live-read gate in - * r_startjob never fires, and the width guard silently uses the default 12mm media */ - papplPrinterSetReasons(p, PAPPL_PREASON_OFFLINE, PAPPL_PREASON_MEDIA_EMPTY); - papplLog(sys, p ? PAPPL_LOGLEVEL_INFO : PAPPL_LOGLEVEL_ERROR, - "%s offline printer for %s (no device at startup; awaiting connection)", - p ? "Created" : "FAILED to create", uri); - } + /* Always create the configured supported model's stable queue. Discovery + * supplies diagnostics, never a first-device fallback or a different model. */ + const pt_dev *model = pt_usb_supported_model(getenv("PTOUCH_MODEL")); + papplDeviceList(PAPPL_DEVTYPE_CUSTOM_LOCAL, discovery_cb, NULL, discovery_error_cb, sys); + ssize_t matches = pt_usb_count(model->vid, model->pid); + pappl_printer_t *p = papplPrinterCreate(sys, 0, "ptouch", "brother_ptouch", + "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", + "ptouch://Brother/PT-2730"); + if (p && matches != 1) + papplPrinterSetReasons(p, PAPPL_PREASON_OFFLINE, PAPPL_PREASON_NONE); + papplLog(sys, p ? PAPPL_LOGLEVEL_INFO : PAPPL_LOGLEVEL_ERROR, + "%s printer for ptouch://Brother/PT-2730 (%zd matching USB devices)", + p ? "Created" : "FAILED to create", matches); return sys; } @@ -138,15 +122,16 @@ static void smoke_err_cb(const char *message, void *data) static int run_smoke(void) { pt_usb_register(); - pt_found found = {0}; - papplDeviceList(PAPPL_DEVTYPE_CUSTOM_LOCAL, grab_cb, &found, smoke_err_cb, NULL); - if (!found.found) { - fprintf(stderr, "smoke: no ptouch device found\n"); + const char *uri = "ptouch://Brother/PT-2730"; + const pt_dev *model = pt_usb_supported_model(getenv("PTOUCH_MODEL")); + ssize_t count = pt_usb_count(model->vid, model->pid); + if (count != 1) { + fprintf(stderr, "smoke: expected one PT-2730, found %zd (none or ambiguous)\n", count); return 2; } - pappl_device_t *dev = papplDeviceOpen(found.uri, "smoke", smoke_err_cb, NULL); + pappl_device_t *dev = papplDeviceOpen(uri, "smoke", smoke_err_cb, NULL); if (!dev) { - fprintf(stderr, "smoke: open failed for %s\n", found.uri); + fprintf(stderr, "smoke: open failed for %s\n", uri); return 3; } uint8_t init[8]; @@ -157,12 +142,18 @@ static int run_smoke(void) papplDeviceGetMetrics(dev, &m); papplDeviceClose(dev); printf("smoke: device write_bytes=%zu write_requests=%zu to %s\n", - m.write_bytes, m.write_requests, found.uri); + m.write_bytes, m.write_requests, uri); return (!smoke_saw_error && m.write_bytes >= n) ? 0 : 1; /* 1 = no real write */ } int main(int argc, char *argv[]) { + const char *configured = getenv("PTOUCH_MODEL"); + if (!pt_usb_supported_model(configured)) { + fprintf(stderr, "PTOUCH_MODEL=%s is %s; only PT-2730 is print-supported\n", + configured, pt_lookup_name(configured) ? "recognized but unsupported" : "unknown"); + return EXIT_FAILURE; + } if (argc >= 2 && strcmp(argv[1], "smoke") == 0) return run_smoke(); return papplMainloop(argc, argv, "1.0", NULL, diff --git a/src/tables.c b/src/tables.c index 5da6f29..15c05ec 100644 --- a/src/tables.c +++ b/src/tables.c @@ -67,3 +67,25 @@ int pt_tape_px(int mm) } return 0; } + +bool pt_tape_geometry(int tape_mm, pt_geometry *out) +{ + if (!out) return false; + memset(out, 0, sizeof *out); + int px = pt_tape_px(tape_mm); + if (px <= 0 || px > 128) return false; + out->nominal_px = tape_mm * 100 * 180 / 2540; + out->imageable_px = px; + out->left_px = (out->nominal_px - px) / 2; + out->right_px = out->left_px + px; + /* Ceiling round-trips to the exact imageable pixel width. */ + int printable_centimm = (px * 2540 + 179) / 180; + out->margin_centimm = (tape_mm * 100 - printable_centimm) / 2; + out->left_margin_centimm = (out->left_px * 2540 + 179) / 180; + out->right_margin_centimm = ((out->nominal_px - out->right_px) * 2540 + 179) / 180; + if (out->left_margin_centimm < out->margin_centimm) + out->left_margin_centimm = out->margin_centimm; + if (out->right_margin_centimm < out->margin_centimm) + out->right_margin_centimm = out->margin_centimm; + return true; +} diff --git a/src/tables.h b/src/tables.h index b526feb..c83100d 100644 --- a/src/tables.h +++ b/src/tables.h @@ -2,6 +2,7 @@ #define PT_TABLES_H #include +#include /* Per-model protocol flags (from upstream ptouch-print). */ #define FLAG_NONE 0x00u @@ -27,4 +28,12 @@ const pt_dev *pt_lookup_name(const char *name); /* Imageable px for a tape width in mm; 0 if unknown. */ int pt_tape_px(int mm); +/* PT-2730's canonical physical window at 180 dpi. right_px is exclusive; + * margin_centimm is the physical minimum, with pixel-aligned published margins. */ +typedef struct { + int nominal_px, imageable_px, left_px, right_px, margin_centimm; + int left_margin_centimm, right_margin_centimm; +} pt_geometry; +bool pt_tape_geometry(int tape_mm, pt_geometry *out); + #endif /* PT_TABLES_H */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7fdf761..abf40bd 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,3 +1,6 @@ +# Keep test assertions active without changing production compiler options. +add_compile_options(-UNDEBUG) + add_executable(test_tables test_tables.c) target_link_libraries(test_tables ptcore) add_test(NAME tables COMMAND test_tables) @@ -21,8 +24,6 @@ add_test(NAME cutter COMMAND test_cutter) # Test-only command-sequence probe; capture generation needs only the core. add_executable(ptouch-cut-probe cut_probe.c) target_link_libraries(ptouch-cut-probe PRIVATE ptcore) -find_package(Python3 COMPONENTS Interpreter) -if(Python3_Interpreter_FOUND) - add_test(NAME cut_probe_capture COMMAND ${Python3_EXECUTABLE} - ${CMAKE_CURRENT_SOURCE_DIR}/test_cut_probe.py $) -endif() +find_package(Python3 REQUIRED COMPONENTS Interpreter) +add_test(NAME cut_probe_capture COMMAND ${Python3_EXECUTABLE} + ${CMAKE_CURRENT_SOURCE_DIR}/test_cut_probe.py $) diff --git a/tests/CUT-PROBE.md b/tests/CUT-PROBE.md index 367de8a..c478abc 100644 --- a/tests/CUT-PROBE.md +++ b/tests/CUT-PROBE.md @@ -3,7 +3,9 @@ The optional `ptouch-cut-probe` target exercises cutter command sequences without changing the application driver. Socket captures verify bytes; real USB tests and physical observations establish whether labels print and cut correctly. -Only the Brother PT-2730 is hardware-certified. +Only the Brother PT-2730 has physical evidence. The four-mode `batch` gate +below passed its bounded eight-label check on 24 mm tape; the separate 21-label integrated application +and recovery acceptance also passed on 12 mm tape. ## Build and run the probe @@ -31,11 +33,155 @@ printing or cutting. Stop a failed test without automatically replaying it. `--status-only` sends a status request without initialization, raster, feeding, or cutting. `--wait-ready 1` consumes unsolicited status after each page and waits for receiving-ready: type 06, phase 00, zero phase number. The loop is -bounded to 70 reads with a 300 ms read timeout and sends no requests while +bounded to approximately twenty seconds with a 300 ms read timeout and sends no requests while printing. +## Four-mode candidate protocol gate + +`--sequence batch` exercises the proposed deferred-command lifecycle without +changing the production driver or using its copy-based planner. It prepares each +label with print information, autocut flags, and chain-on; supplies all raster; +and leaves that label's print command pending until the next label starts or the +batch succeeds. Intermediate labels receive FF. A continuing batch ends with FF; +a finishing batch changes chain-off after the last raster and sends one EJECT. +There is no empty-page FF/EJECT pair. Mandatory precut enables autocut only on the +first label, unless the mode also requests cuts between later labels. + +| Probe mode | Cut between labels | Cut after batch | First label precut | +| --- | --- | --- | --- | +| `chain` | Yes | No | Yes | +| `each` | Yes | Yes | Yes | +| `none` | No | No | Yes | +| `end` | No | Yes | Yes | + +Each batch is bounded to one, two, or three labels. `--batches` accepts at most +three batches with at most six labels total, expanding each batch separately +without resetting between healthy unfinished batches. `--continue 1` omits the +first initialization only for a known healthy open run from a successful earlier +probe. These are test-only controls, not application configuration or IPP options. +The candidate rejects precut off and requires readiness waiting for USB output. + +Generate fixtures safely without a device argument: + +```sh +ptouch-cut-probe --capture /tmp/batch-end-3.prn --sequence batch \ + --labels 3 --mode end --precut 1 --tape-mm 24 +ptouch-cut-probe --capture /tmp/batch-chain-matrix.prn --sequence batch \ + --batches chain:3,chain:2,each:1 --precut 1 --tape-mm 24 +ptouch-cut-probe --capture /tmp/batch-none-matrix.prn --sequence batch \ + --batches none:2,none:2,end:1 --precut 1 --tape-mm 24 +python3 tests/test_cut_probe.py --inspect /tmp/batch-end-3.prn +``` + +The Python capture suite verifies four modes at all three bounded label counts, +one print command per rendered label, late finalization ordering, mandatory +first-label-only precut in final-only/no-cut modes, resets, and both continuation +matrices. Raster payload is decoded by length, so command-like pixel values do +not create false cuts. Unique stripe counts distinguish labels; edge bars mark +the full intended length. Captures establish the transferred command policy, +not whether this model interprets the final command as intended. + +### Approved eight-label initial gate + +The user approved the reduced initial gate: four two-label batches on 24 mm +tape. The earlier 27-label proposal is superseded. This gate checks all four +modes and continuation; the separate integrated acceptance matrix subsequently +received its own authorization and passed on 12 mm tape. + +Stop the idle printer server to release the USB interface. Confirm a clean live +status and 24 mm cassette, and power-cycle before the first fresh run. A later +fresh run can initialize after the preceding final cut is physically confirmed +and receiving-ready was acknowledged; power-cycle after any uncertain output. +Use the existing compatible runtime for this test-only probe; do not replace +the deployed server image. Retain captures and stdout/stderr for every command. + +First run two chain-only labels: + +```sh +ptouch-cut-probe --capture /tmp/gate-chain-2.prn --sequence batch \ + --labels 2 --mode chain --precut 1 --tape-mm 24 --wait-ready 1 \ + --device ptouch://Brother/PT-2730 +``` + +Check one initial leader, label 1 separated, and label 2 held. After a short idle +gap, continue without resetting: + +```sh +ptouch-cut-probe --capture /tmp/gate-each-2.prn --sequence batch \ + --labels 2 --mode each --precut 1 --tape-mm 24 --wait-ready 1 --continue 1 \ + --device ptouch://Brother/PT-2730 +``` + +Both new labels and the previous held end should be released, with no discarded +leader between batches. This checks late chain-off after a multi-label batch. + +After confirming the preceding final cut, start a fresh run with two no-cut labels: + +```sh +ptouch-cut-probe --capture /tmp/gate-none-2.prn --sequence batch \ + --labels 2 --mode none --precut 1 --tape-mm 24 --wait-ready 1 \ + --device ptouch://Brother/PT-2730 +``` + +Check one initial leader and one joined, held two-label strip. Continue after a +short idle gap without resetting: + +```sh +ptouch-cut-probe --capture /tmp/gate-end-2.prn --sequence batch \ + --labels 2 --mode end --precut 1 --tape-mm 24 --wait-ready 1 --continue 1 \ + --device ptouch://Brother/PT-2730 +``` + +The new batch's precut should release the prior strip. The new two labels should +remain joined and be cut free only at batch end, with no discarded inter-batch +leader. Restore the original server after the probes complete safely. + +Each batch uses 180/216 rows at 180 dpi, framing 25.4/30.48 mm. Eight labels total +223.52 mm of framed raster, plus two fresh-run leaders and physical feed margins. +This is approximately 22 cm of label content; record actual tape use. There is +no image decoder or production leading-row correction in this direct probe. + +The same eight-label command policy can be checked without USB access using +`--batches chain:2,each:2` and `--batches none:2,end:2`, omitting `--device` and +`--wait-ready`. Captures must show one initialization and one EJECT per pair, +with first-label precut and the expected later-label autocut flags. Single-label, +three-label, and other transition cases remain covered in the capture suite; +they do not consume tape in this initial gate. + +Record loaded tape, mode order, stripe/edge visibility, leader count, each +separation, held final ends, measured lengths, readiness/error packets, and +any discarded tape. Pause for physical observations after each batch. Any +transfer failure, printer error, readiness timeout, or incorrect physical output +stops the gate immediately; do not automatically replay, feed, cut, or reset. +Power-cycle before further work after uncertain output. Revise an unsuccessful +command policy before enabling it in production, retaining all required modes +and unknown-count support. + ## Diagnostic findings +### Eight-label gate result + +The user confirmed these physical results for the approved initial gate: + +| Batch | Observation | +|-------|-------------| +| Two chain-only labels | Initial leader cut off; first rectangle separated; final rectangle held | +| Two chain-and-final labels | Prior held rectangle released, both new labels separated, final cut; no discarded leader | +| Two no-cut labels | Initial leader cut off; rectangles remained joined and the end held | +| Two final-only labels | Prior double-label strip released, new double-label strip cut free; no internal cut or discarded leader | + +All eight labels received clean completion and receiving-ready notifications. +Captures contain the expected FF/EJECT commands and no initialization between +healthy continued batches. The deployed server image was not replaced, and the +original server was restored afterward. Evidence lives in +`vault:/tmp/ptouch-eight-label-gate.MDnnuO/` and the OpenSpec change's validation.md. +The separately authorized 21-label integrated acceptance subsequently passed +on 12 mm tape, including PNG/raster batches and recovery. See +HARDWARE-ACCEPTANCE.md and the change's validation.md. Software-only geometry +coverage for 6/9/18 mm does not claim physical certification. + +### Earlier probes + The original queue-drain finalization proposal was superseded by explicit job finishings. Its probes remain useful evidence: @@ -61,7 +207,7 @@ and the [PT-2730-compatible ptouch-rs implementation](https://github.com/vowstar help interpret these commands. Physical PT-2730 observations remain the evidence for this printer's behavior. -## Verified finishing matrix +## Historical verified finishing matrix Tests used 24 mm tape and a framed X fixture with 180 rows at 180 dpi. Software checks cover command captures, resolved IPP options, checked transfers, and status @@ -77,6 +223,10 @@ handling; the following results were observed on the physical printer: | None, none, trim with intervening status queries | Joined strip preserved; same cuts as the idle-gap test | | Single trim label | Leader, cut, X label, cut | +These observations concern the earlier two-mode production behavior, including +optional precut. They do not establish the revised four-mode contract, mandatory +precut, or separation of consecutive none batches without wasted leaders. + The printed frame's long edge measured approximately one inch, consistent with 25.4 mm for 180 rows at 180 dpi. No extra blank label was observed. A precut-off single-copy trim job kept its leader attached and cut its final label free. diff --git a/tests/FINISHINGS.md b/tests/FINISHINGS.md index d46b321..b2a2b23 100644 --- a/tests/FINISHINGS.md +++ b/tests/FINISHINGS.md @@ -1,86 +1,68 @@ -# Finishing-option verification +# Finishing verification -The PAPPL-only `driver_finishings` CTest target uses real PAPPL 1.4.11 objects -and IPP Create-Job requests. Its transport fixture injects raster, FF, and EJECT -write failures and fragmented printer status packets. It verifies that buffered -acceptance cannot hide those failures and that repeated cleanup emits nothing. +The contract is one IPP job per batch, four policies, mandatory first-label +precut, and no discarded leaders during healthy continuation. Registered enum +choices are none (3), trim (11), trim-after-pages (60), and trim-after-job (63). +Chain-only uses the implementation-defined ptouch-chain template through +standard finishings-col. Collection no-value selects none. -The same target uploads generated PNGs through IPP Send-Document, releases them -through PAPPL scheduling, and captures output using PAPPL's actual TCP device -scheme. Only the printer's online media/status callback is fixed in this test; -production status behavior is exercised separately with injected USB presence -and tape responses. +## Software checks -Verified scenarios: - -- omitted finishings uses the advertised none default; -- none and trim enum choices and finishings-col templates reach the driver; -- three none copies end with three FFs and no EJECT or autocut; -- three trim copies end with two FFs and one EJECT; -- no-cut state survives a handle close/reopen and a gap beyond the old idle timer; -- canceled, malformed, and unsupported followers do not finalize a no-cut run; -- two jobs already Pending before printing both finalize when both request trim; -- legacy PTOUCH_CUT_MODE cannot override either explicit choice; -- partial status reads consume printing, print-completed, then receiving-ready; -- detected printer error, malformed status, and cancellation fail readiness; -- state exhaustion refuses a no-cut job before output; -- USB disappearance clears stale run/media state, and the existing refresh path - consumes an old phase notification, then confirms 24 mm from the requested - snapshot before creating the next page's nominal-width geometry; -- strict width mismatch and unsupported raw raster input remain rejected. - -Run in the deploy build stage, which provides PAPPL/libusb/libpng: +Run the full build in the pinned container environment: ```sh -cmake -S . -B build +cmake -S . -B build -DPTOUCH_CORE_ONLY=OFF cmake --build build ctest --test-dir build --output-on-failure ``` -The 2026-10-11 run passed all six container CTest suites. The host's seven -protocol-capture tests also passed. The complete deploy image built and its -localhost IPP smoke query reported supported finishings none/trim and default -none. These are software results, not certification of physical cuts or leaders. -PT-2730 validation remains recorded separately in CUT-PROBE.md and the OpenSpec -tasks, and integration waits for that gate. +The hardware-free cutter and capture suites check the two policy flags and +observable command order, including one/three labels and the eight-label gate. +They use no copy-sized operation array or planner API. Historical diagnostic +probe sequences remain test-local. + +Real IPP driver tests exercise actual PNG copies and unknown-count raster labels, +all four presets, aliases, defaults, explicit collections, supported attributes, +and rejected/conflicting requests. A counting sink verifies 65 and 999 PNG copies; +streaming formats advertise one copy. Failure fixtures cover raster, FF, EJECT, +decode, cancellation, and fragmented receiving-ready. Repeated cleanup must +emit nothing; clean status/reopen must not clear uncertain output. + +The minimal PAPPL finishing fixture isolates advertised-attribute acceptance, +legacy-driver compatibility, and atomic enum/collection default updates. The +raster fixture covers malformed first/next headers, incomplete rows/pages, and +negative input callbacks before successful end-job cleanup. -The real-USB candidate test on 2026-10-11 completed three trim copies with zero -printer error bits and a receiving-ready notification consumed after each copy. -Physical output observations and the remaining hardware matrix are pending; see -CUT-PROBE.md. +`python3 scripts/smoke-image.py IMAGE` runs the built deploy image, submits real +IPP PNG jobs for all four policies and the omitted default, and captures actual +socket output. It checks explicit 50 mm pages, copies, public capabilities, and +malformed-image failure without USB access. This requires Docker and ipptool. +`scripts/check-cups.py /path/to/ptouch-app` runs inside an isolated container +with CUPS, cups-filters, Ghostscript, ipptool, and Pillow. It creates an ordinary +driverless queue, submits PNG/JPEG, two-label PDFs, and two copies of a PDF with registered finishing +choices, queues two jobs while the socket printer is offline, then captures their +resumption. It also checks standard chain collection submission and that saving +unrelated fields in the existing Web UI preserves trim/final-only defaults. +Custom-size cases cover CUPS width rounding with fitting explicit-margin content, +oversized rejection without output, and exact standard media-col dimensions. -On 2026-10-12 the observed precut-disabled three-copy test exposed joined final -two copies. A failing cutter regression and local correction keep autocut off -for the first copy and on for later copies, including the last. All six container -suites and seven host capture tests pass; the user observed all three corrected -copies cut apart, with leading tape attached to the first copy. +The change's validation.md records execution results separately from physical +printer observations. -The CI smoke's terse ipptool command exited zero and printed PASS, but its grep -required a successful-ok string only printed in verbose mode. Its final log grep -could also fail before the container's log follower emitted startup lines. The -local workflow now uses the IPP test's exit status, prints a verbose final query, -and keeps container logs as diagnostics. The corrected shell block passed locally -with bash -e -o pipefail. The published workflow subsequently passed GitHub CI -on commit 688ffd4 (run 38278139208). +## Physical evidence -The 2026-10-12 hardware gate is now complete: the user observed explicit -none/none/trim with idle gaps, separated trim copies with precut enabled and -disabled, approximately one-inch frame length, and successful printing and -cutting of a queued trim job after switching the printer on. See CUT-PROBE.md -for the observations and logs. The final deploy image also built successfully -and passed the corrected workflow smoke locally. This validates publication -of the final correction. +The approved eight-label direct-protocol gate passed on the PT-2730 with 24 mm +tape: chain:2 then each:2, and none:2 then end:2, with an observation/idle gap at +each batch boundary. The operator confirmed initial leaders cut off, expected +internal/final cuts, held ends released by the following batch, and no discarded +inter-batch leaders. See CUT-PROBE.md for exact commands and the change's +validation.md for status packets, captures, and operator observations. -Final review found that skipping all idle status polling would make cassette -width changes invisible until USB disconnection. A new regression failed -against that implementation. The local correction requests status without -ESC @ while a run remains open, both during idle polling and before creating -the next PNG job's geometry. Tests confirm same-width continuation, changed -width publication and chain clearing, and empty cassette detection, with only -the three-byte status request on the wire. All six CTest suites pass with the -correction. The full deploy image built and passed the corrected smoke. The -user confirmed the expected joined none/none strip and separately finalized -trim label after status queries and idle gaps, completing physical validation -before publication. The follow-up review reported no remaining actionable -defects. +The separately authorized integrated acceptance also passed all 21 labels on +12 mm tape, including PNG/raster batches, cassette change, offline continuation, +uncertain-output blocking, and power-cycle/server-restart recovery without ghost +output. The operator confirmed length/orientation markers and all four cut +policies. Other tape widths have software coverage only, except the 24 mm direct +protocol gate. See HARDWARE-ACCEPTANCE.md and the change's validation.md. The +deployed service image has not been replaced. diff --git a/tests/HARDWARE-ACCEPTANCE.md b/tests/HARDWARE-ACCEPTANCE.md new file mode 100644 index 0000000..be04b1c --- /dev/null +++ b/tests/HARDWARE-ACCEPTANCE.md @@ -0,0 +1,75 @@ +# Remaining physical acceptance + +Status: **passed** on 2026-10-13 with 12 mm tape, all 21 complete labels and +negative/recovery trials. Operator confirms the layouts. The original service is +restored, with its image/configuration unchanged. The completed eight-label direct +protocol gate remains passed. This session checks the integrated candidate image +through ordinary IPP and verifies explicit recovery. It does not replace the +running service permanently. + +## Bound and tape budget + +Start with the operator-confirmed 12 mm cassette. If other widths are available, reuse +counted batches after a confirmed final cut for width/orientation checks; adapt +the imageable fixture to that cassette without adding labels. Send exactly +21 complete labels in the matrix +below, plus one deliberately incomplete raster upload and one blocked resubmission. +The latter two must issue no FF/EJECT; the incomplete upload can leave raster data +in the printer and is followed by explicit power-cycle recovery. + +Most labels have 180 rows at 180 dpi, or 25.4 mm. The three-label raster document +uses 180/216/180 rows to expose individual lengths without a total-count attribute. +Printed content totals about **54 cm**. This excludes initial leaders and physical +feed allowances, which were not measured in the earlier gate; it is not a promise +of total consumed tape. No additional labels or repeats are part of this approval. +Stop and report any failed criterion before revising or repeating a stage. + +## Matrix + +| Stage | Submission | Labels | Required observation | +| --- | --- | ---: | --- | +| 1 | PNG, chain-only | 3 | First leader removed, labels separated, final end held | +| 2 | PNG, chain-only after an idle gap | 2 | Prior held label released, no discarded inter-batch leader, final end held | +| 3 | PNG, chain-and-final | 1 | Prior held end released, final label cut free | +| 4 | PNG, none | 2 | Initial leader removed, joined strip held | +| 5 | PNG, none | 2 | Prior strip released, second joined strip held, no discarded inter-batch leader | +| 6 | PNG, final-only | 1 | Prior strip released, final label cut free | +| 7 | One PNG with three copies, chain-and-final | 3 | Exactly three separated labels, one initial precut, final end cut | +| 8 | Three unknown-count raster labels, chain-and-final | 3 | Three complete separated labels with correct individual lengths/orientation markers, final end cut | +| 9 | One PNG with three copies, final-only | 3 | One joined three-label strip, cut only at batch end | +| 10 | Recovery label after the sequence below | 1 | Only the new marked label prints, with no buffered fragment or ghost replay | +| | **Complete labels** | **21** | | + +Queue stages 2 and 3 while USB is disconnected, inspect their pending state, +then reconnect and inspect each batch before releasing the next. This reuses the +counted labels for physical offline recovery and healthy continuation. Confirm +cassette removal/reinsertion and readiness using status queries; mismatched-media +requests must fail without raster. Physical width certification is limited to +available cassettes; retain the existing distinction between five-width software +coverage and actual hardware evidence. + +For uncertain-output recovery, after stage 9 send a single-page raster with a +valid header but an incomplete body. Confirm an aborted job with no print/feed +command. Submit the marked recovery label: it must be blocked without new output. +A clean status or USB reopen must not clear this condition. Power-cycle the printer, +then restart only the candidate server, and explicitly submit that recovery label +again for stage 10. Inspect failed job records before restart; process-local queue +persistence and automatic replay are not promised. + +## Execution boundary + +Before starting, record the current deployed image and confirm its queue is idle. +Temporarily yield USB to the isolated candidate, retaining the previous image and +configuration. Use one candidate process across healthy continued batches, with +no automatic reset or idle finalization. Capture job attributes, printer status, +and command data separately from the operator's physical observations. Restore +the original service only after the run is confirmed closed and recovery is safe. + +Any changed label count, repeat, other tape width that needs extra printed labels, +or permanent deployment requires a separate instruction. Archive the OpenSpec +change only after the required physical evidence is recorded. + +Evidence: `vault:/tmp/ptouch-integrated-acceptance.Nj73Wz/`, with a local +copy at `/tmp/ptouch-integrated-evidence.tar.gz`; detailed operator observations +and per-stage captures are recorded in the change's validation.md. Actual tape +consumption was not measured. No further physical labels are authorized. diff --git a/tests/cut_probe.c b/tests/cut_probe.c index 158924f..46660fb 100644 --- a/tests/cut_probe.c +++ b/tests/cut_probe.c @@ -1,10 +1,12 @@ /* Test-only PT-2730 finalization probe. Never used by the production app. */ +#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include +#include #include "cutter.h" #include "protocol.h" #include "raster.h" @@ -19,15 +21,18 @@ typedef struct { size_t bytes; #ifdef PT_PROBE_TRANSPORT pappl_device_t *device; + const char *device_uri; #endif } output; static void usage(const char *name) { fprintf(stderr, - "Usage: %s --capture FILE --sequence baseline|drain|tail-eject|tail-chain-off " - "--labels 1|3 --mode each|end --precut 0|1 --tape-mm N " + "Usage: %s --capture FILE --sequence batch|baseline|drain|tail-eject|tail-chain-off " + "--labels 1|2|3 --mode chain|each|none|end --precut 0|1 --tape-mm N " "[--device ptouch://Brother/PT-2730|socket://HOST:PORT]\n" + "Batch requires --precut 1; --continue 1 omits reset for a known healthy open run.\n" + "Batch also accepts --batches chain:3,chain:2,each:1 instead of --labels/--mode.\n" "Add --wait-ready 1 to read USB completion status between pages.\n" "Omitting --device only writes a capture; it never contacts a printer.\n" "Use --status-only to query the PT-2730 without initialization or printing.\n", @@ -52,6 +57,15 @@ static void report_error(const char *message, void *data) } #endif +#ifdef PT_PROBE_TRANSPORT +static int64_t milliseconds(void) +{ + struct timespec now; + if (clock_gettime(CLOCK_MONOTONIC, &now)) return -1; + return (int64_t)now.tv_sec * 1000 + now.tv_nsec / 1000000; +} +#endif + /* Diagnostic only: do not send another page while the printer is busy. * Read unsolicited packets, never issue a status request during printing. */ static bool read_ready(output *out, bool wait, int page_index) @@ -60,8 +74,12 @@ static bool read_ready(output *out, bool wait, int page_index) if (!out->device) return true; uint8_t packet[32]; size_t used = 0; - for (int attempt = 0; attempt < 70; attempt++) { - ssize_t n = pt_usb_read_status(out->device, packet + used, sizeof packet - used); + int64_t started = milliseconds(); + if (started < 0) return false; + /* Leave room for the final bounded 300 ms read within twenty seconds. */ + while (milliseconds() >= started && milliseconds() - started < 19700) { + ssize_t n = pt_usb_read_status(out->device, out->device_uri, + packet + used, sizeof packet - used); if (n < 0) { fprintf(stderr, "probe: status read failed after page %d\n", page_index + 1); return false; @@ -135,49 +153,92 @@ static bool page(output *out, int index, int width) return true; } +typedef struct { pt_cut_mode mode; int labels; } batch; + +static int parse_mode(const char *value) +{ + if (!strcmp(value, "chain")) return PT_CUT_CHAIN; + if (!strcmp(value, "each")) return PT_CUT_EACH; + if (!strcmp(value, "none")) return PT_CUT_NONE; + if (!strcmp(value, "end")) return PT_CUT_END; + return -1; +} + +/* At most three batches and six labels: enough for both continuation gates. */ +static int parse_batches(const char *value, batch *batches) +{ + const char *p = value; + int count = 0, total = 0; + while (*p && count < 3) { + const char *colon = strchr(p, ':'); + char mode[6]; + if (!colon || colon == p || colon - p >= (int)sizeof mode) return -1; + memcpy(mode, p, (size_t)(colon - p)); + mode[colon - p] = 0; + int parsed = parse_mode(mode); + if (parsed < 0 || colon[1] < '1' || colon[1] > '3' || + (colon[2] && colon[2] != ',')) return -1; + batches[count++] = (batch){parsed, colon[1] - '0'}; + total += colon[1] - '0'; + if (!colon[2]) return total <= 6 ? count : -1; + p = colon + 3; + } + return -1; +} + +/* Model the proposed callbacks: rendpage leaves one pending print command; + * the next valid label releases it with FF, successful rendjob selects FF or + * late chain-off/EJECT. No operation array or count-dependent page setup. */ +static bool run_batch(output *out, int labels, int tape, pt_cut_mode mode, + bool continuing, bool wait_ready) +{ + uint8_t bytes[8]; + bool pending = false; + pt_cut_policy policy = pt_cut_policy_for(mode); + if ((!continuing && !emit(out, bytes, pt_cmd_init(bytes))) || + !emit(out, bytes, pt_cmd_rasterstart(bytes, FLAG_NONE))) return false; + for (int i = 0; i < labels; i++) { + if (pending && (!emit(out, bytes, pt_cmd_ff(bytes)) || + (wait_ready && !read_ready(out, true, i - 1)))) return false; + pending = false; + if (!emit(out, bytes, pt_cmd_printinfo(bytes, (uint8_t)tape)) || + !emit(out, bytes, pt_cmd_setmode(bytes, pt_cut_autocut(policy, i == 0) ? 0x40 : 0)) || + !emit(out, bytes, pt_cmd_setadvanced(bytes, 0)) || + !page(out, i, pt_tape_px(tape))) return false; + pending = true; + } + if (pending) { + if (policy.cut_at_end && !emit(out, bytes, pt_cmd_setadvanced(bytes, 8))) return false; + if (!emit(out, bytes, policy.cut_at_end ? pt_cmd_eject(bytes) : pt_cmd_ff(bytes)) || + (wait_ready && !read_ready(out, true, labels - 1))) return false; + } + return true; +} + +/* Preserve the historical diagnostic bytes, including optional precut and + * the empty drain EJECT. These commands are test-local, not production policy. */ static bool run(output *out, int labels, int tape, pt_cut_mode mode, int precut, int sequence, bool wait_ready) { - pt_op ops[15]; - int count = pt_plan_batch(labels, (uint8_t)tape, mode, precut, - sequence == 1 || sequence == 2, ops, 15); uint8_t bytes[8]; - if (wait_ready && !read_ready(out, false, -1)) return false; - if (count < 0 || !emit(out, bytes, pt_cmd_init(bytes)) || - !emit(out, bytes, pt_cmd_rasterstart(bytes, FLAG_NONE))) - return false; + if ((wait_ready && !read_ready(out, false, -1)) || + !emit(out, bytes, pt_cmd_init(bytes)) || + !emit(out, bytes, pt_cmd_rasterstart(bytes, FLAG_NONE))) return false; - for (int i = 0; i < count; i++) { - size_t n; - switch (ops[i].kind) { - case PT_OP_PRINTINFO: n = pt_cmd_printinfo(bytes, ops[i].arg); break; - case PT_OP_SETMODE: n = pt_cmd_setmode(bytes, ops[i].arg); break; - case PT_OP_SETADVANCED: - n = pt_cmd_setadvanced(bytes, sequence == 3 && ops[i].page_index == labels - 1 - ? 0 : ops[i].arg); - break; - case PT_OP_FF: - /* Isolate the failed probe's empty second print command: - * choose the last page's terminator after its raster, once. */ - n = sequence == 2 && i == count - 1 ? pt_cmd_eject(bytes) : pt_cmd_ff(bytes); - break; - case PT_OP_EJECT: - if (sequence == 3 && !emit(out, bytes, pt_cmd_setadvanced(bytes, 8))) - return false; - n = pt_cmd_eject(bytes); - break; - case PT_OP_PRINT_PAGE: - if (!page(out, ops[i].page_index, pt_tape_px(tape))) - return false; - continue; - default: return false; - } - if (!emit(out, bytes, n)) - return false; - if (wait_ready && (ops[i].kind == PT_OP_FF || ops[i].kind == PT_OP_EJECT) && - !read_ready(out, true, ops[i].page_index)) return false; + for (int i = 0; i < labels; i++) { + bool autocut = precut || (mode == PT_CUT_EACH && i > 0); + bool terminal = i == labels - 1 && sequence != 1 && sequence != 2; + if ((autocut && !emit(out, bytes, pt_cmd_printinfo(bytes, (uint8_t)tape))) || + !emit(out, bytes, pt_cmd_setmode(bytes, autocut ? 0x40 : 0)) || + !emit(out, bytes, pt_cmd_setadvanced(bytes, terminal && sequence != 3 ? 8 : 0)) || + !page(out, i, pt_tape_px(tape))) return false; + if (terminal && sequence == 3 && + !emit(out, bytes, pt_cmd_setadvanced(bytes, 8))) return false; + /* tail-eject replaces the final FF instead of appending an empty EJECT. */ + bool eject = terminal || (sequence == 2 && i == labels - 1); + if (!emit(out, bytes, eject ? pt_cmd_eject(bytes) : pt_cmd_ff(bytes)) || + (wait_ready && !read_ready(out, true, i))) return false; } - /* No sleep, reset, extra raster, or device close between final FF and EJECT. */ return sequence != 1 || emit(out, bytes, pt_cmd_eject(bytes)); } @@ -191,10 +252,12 @@ static int status_only(void) if (!out.capture) return 1; out.device = papplDeviceOpen("ptouch://Brother/PT-2730", "cut-probe-status", report_error, NULL); + out.device_uri = "ptouch://Brother/PT-2730"; bool ok = out.device && emit(&out, command, pt_cmd_status_request(command)); size_t received = 0; for (int i = 0; ok && i < 20 && received < sizeof response; i++) { - ssize_t n = pt_usb_read_status(out.device, response + received, sizeof response - received); + ssize_t n = pt_usb_read_status(out.device, out.device_uri, + response + received, sizeof response - received); if (n < 0) ok = false; else received += (size_t)n; } @@ -223,8 +286,9 @@ int main(int argc, char **argv) #endif } const char *capture = NULL, *uri = NULL; - int labels = -1, tape = -1, precut = -1, wait_ready = 0; - pt_cut_mode mode = PT_CUT_NONE; + int labels = -1, tape = -1, precut = -1, wait_ready = 0, continuing = 0; + int mode = -1; + const char *batch_list = NULL; int drain = -1; for (int i = 1; i < argc; i++) { if (!strcmp(argv[i], "--help")) { @@ -242,21 +306,33 @@ int main(int argc, char **argv) else if (!strcmp(key, "--tape-mm")) tape = number(value); else if (!strcmp(key, "--precut")) precut = number(value); else if (!strcmp(key, "--wait-ready")) wait_ready = number(value); + else if (!strcmp(key, "--continue")) continuing = number(value); + else if (!strcmp(key, "--batches")) batch_list = value; else if (!strcmp(key, "--sequence") && !strcmp(value, "baseline")) drain = 0; else if (!strcmp(key, "--sequence") && !strcmp(value, "drain")) drain = 1; else if (!strcmp(key, "--sequence") && !strcmp(value, "tail-eject")) drain = 2; else if (!strcmp(key, "--sequence") && !strcmp(value, "tail-chain-off")) drain = 3; - else if (!strcmp(key, "--mode") && !strcmp(value, "each")) mode = PT_CUT_EACH; - else if (!strcmp(key, "--mode") && !strcmp(value, "end")) mode = PT_CUT_END; + else if (!strcmp(key, "--sequence") && !strcmp(value, "batch")) drain = 4; + else if (!strcmp(key, "--mode")) mode = parse_mode(value); else { usage(argv[0]); return 2; } } - if (!capture || (labels != 1 && labels != 3) || pt_tape_px(tape) <= 0 || + batch batches[3]; + int batch_count = batch_list ? parse_batches(batch_list, batches) : 1; + if (!batch_list) batches[0] = (batch){mode, labels}; + if (!capture || pt_tape_px(tape) <= 0 || (precut != 0 && precut != 1) || (wait_ready != 0 && wait_ready != 1) || + (continuing != 0 && continuing != 1) || (wait_ready && uri && strcmp(uri, "ptouch://Brother/PT-2730")) || - drain < 0 || mode == PT_CUT_NONE || + drain < 0 || batch_count < 1 || + (batch_list && (drain != 4 || labels != -1 || mode != -1)) || + (!batch_list && (mode < 0 || labels < 1 || labels > 3)) || + (drain == 4 && precut != 1) || + (drain != 4 && (batch_list || continuing || + (mode != PT_CUT_EACH && mode != PT_CUT_END) || labels == 2)) || + (drain == 4 && uri && !strcmp(uri, "ptouch://Brother/PT-2730") && !wait_ready) || (uri && strcmp(uri, "ptouch://Brother/PT-2730") && strncmp(uri, "socket://", 9))) { usage(argv[0]); return 2; @@ -274,6 +350,7 @@ int main(int argc, char **argv) } #ifdef PT_PROBE_TRANSPORT if (uri) { + out.device_uri = uri; pt_usb_register(); out.device = papplDeviceOpen(uri, "cut-probe", report_error, NULL); if (!out.device) { @@ -283,7 +360,19 @@ int main(int argc, char **argv) } } #endif - bool ok = run(&out, labels, tape, mode, precut, drain, wait_ready); + bool ok; + if (drain == 4) { + ok = !wait_ready || read_ready(&out, false, -1); + for (int i = 0; ok && i < batch_count; i++) { + fprintf(stderr, "probe: batch=%d labels=%d continuing=%d\n", + i + 1, batches[i].labels, continuing); + ok = run_batch(&out, batches[i].labels, tape, batches[i].mode, + continuing, wait_ready); + continuing = !pt_cut_policy_for(batches[i].mode).cut_at_end; + } + } else { + ok = run(&out, labels, tape, (pt_cut_mode)mode, precut, drain, wait_ready); + } #ifdef PT_PROBE_TRANSPORT if (out.device) papplDeviceClose(out.device); diff --git a/tests/test_cut_probe.py b/tests/test_cut_probe.py index d8391df..98fffd1 100644 --- a/tests/test_cut_probe.py +++ b/tests/test_cut_probe.py @@ -42,13 +42,19 @@ def decode(data): def summary(commands): pages, rows, final = [], 0, [] + mode, advanced = None, None for name, data in commands: - if name == "raster": + if name == "mode": + mode = data[-1] + elif name == "advanced": + advanced = data[-1] + elif name == "raster": rows += 1 elif name in ("ff", "eject"): if rows: pages.append({"rows": rows, "length_mm": round(rows * 25.4 / 180, 2), - "finish": name}) + "finish": name, "autocut": bool(mode & 64), + "chain_off_at_print": bool(advanced & 8)}) rows = 0 else: final.append(name) @@ -58,6 +64,14 @@ def summary(commands): class CaptureTests(unittest.TestCase): + def matrix(self, batches, extra=()): + with tempfile.TemporaryDirectory() as directory: + path = Path(directory) / "capture.prn" + subprocess.run([PROBE, "--capture", str(path), "--sequence", "batch", + "--batches", batches, "--precut", "1", "--tape-mm", "12", + *extra], check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + return decode(path.read_bytes()) + def capture(self, sequence, labels, mode, precut, tape=12, extra=()): with tempfile.TemporaryDirectory() as directory: path = Path(directory) / "capture.prn" @@ -134,6 +148,88 @@ def test_raster_payload_is_not_commands(self): commands = decode(b"\x47\x10\x00" + payload + b"\x0c\x1a") self.assertEqual([name for name, _ in commands], ["raster", "ff", "eject"]) + def test_batch_four_modes_and_actual_label_markers(self): + for labels in (1, 2, 3): + for mode in ("chain", "each", "none", "end"): + with self.subTest(labels=labels, mode=mode): + commands = self.capture("batch", labels, mode, 1) + result = summary(commands) + self.assertEqual(result["init"], 1) + self.assertEqual(result["terminal_commands_after_pages"], []) + self.assertEqual([p["rows"] for p in result["pages"]], + [180, 216, 252][:labels]) + self.assertEqual([p["autocut"] for p in result["pages"]], + [True] * labels if mode in ("chain", "each") else + [True] + [False] * (labels - 1)) + finishes = ["ff"] * labels + if mode in ("each", "end"): + finishes[-1] = "eject" + self.assertEqual([p["finish"] for p in result["pages"]], finishes) + self.assertEqual([p["chain_off_at_print"] for p in result["pages"]], + [False] * (labels - 1) + [mode in ("each", "end")]) + # First and last row are solid edge bars. The first interior + # row differs per label, making accidental page replay visible. + rasters = [data[3:] for name, data in commands if name == "raster"] + position, interiors = 0, [] + for page in result["pages"]: + pixels = rasters[position:position + page["rows"]] + self.assertEqual(pixels[0], pixels[-1]) + self.assertNotEqual(pixels[0], pixels[2]) + interiors.append(pixels[2]) + position += page["rows"] + self.assertEqual(len(set(interiors)), labels) + if labels in (1, 3): + old = self.capture("baseline", labels, "each", 1) + self.assertEqual([data for name, data in commands if name == "raster"], + [data for name, data in old if name == "raster"]) + names = [name for name, _ in commands] + # Late finalization changes chain state only after final raster. + if mode in ("each", "end"): + self.assertEqual(names[-3:], ["raster", "advanced", "eject"]) + + def test_chain_continuation_has_one_reset_and_one_final_command(self): + commands = self.matrix("chain:3,chain:2,each:1") + result = summary(commands) + self.assertEqual(result["init"], 1) + self.assertEqual(result["eject"], 1) + self.assertEqual(result["terminal_commands_after_pages"], []) + self.assertEqual([p["rows"] for p in result["pages"]], [180, 216, 252, 180, 216, 180]) + self.assertEqual([p["autocut"] for p in result["pages"]], [True] * 6) + self.assertEqual([p["finish"] for p in result["pages"]], ["ff"] * 5 + ["eject"]) + self.assertEqual(sum(name == "rasterstart" for name, _ in commands), 3) + + def test_uncut_strips_preparation_is_once_per_batch(self): + result = summary(self.matrix("none:2,none:2,end:1")) + self.assertEqual(result["init"], 1) + self.assertEqual(result["terminal_commands_after_pages"], []) + self.assertEqual([p["autocut"] for p in result["pages"]], + [True, False, True, False, True]) + self.assertEqual([p["finish"] for p in result["pages"]], ["ff"] * 4 + ["eject"]) + + def test_eight_label_hardware_gate_sequences(self): + for batches, autocuts in (("chain:2,each:2", [True] * 4), + ("none:2,end:2", [True, False, True, False])): + with self.subTest(batches=batches): + commands = self.matrix(batches) + result = summary(commands) + self.assertEqual(result["init"], 1) + self.assertEqual(result["eject"], 1) + self.assertEqual(result["terminal_commands_after_pages"], []) + self.assertEqual([p["rows"] for p in result["pages"]], + [180, 216, 180, 216]) + self.assertEqual([p["autocut"] for p in result["pages"]], autocuts) + self.assertEqual([p["finish"] for p in result["pages"]], + ["ff", "ff", "ff", "eject"]) + self.assertEqual([p["chain_off_at_print"] for p in result["pages"]], + [False, False, False, True]) + + def test_continuation_omits_only_init(self): + fresh = self.capture("batch", 3, "end", 1) + continued = self.capture("batch", 3, "end", 1, extra=("--continue", "1")) + self.assertEqual(continued, fresh[1:]) + self.assertEqual(self.matrix("none:2,none:2,end:1", extra=("--wait-ready", "1")), + self.matrix("none:2,none:2,end:1")) + def test_readiness_wait_does_not_add_print_commands(self): baseline = self.capture("tail-chain-off", 3, "each", 1, tape=24) self.assertEqual(self.capture("tail-chain-off", 3, "each", 1, tape=24, @@ -150,7 +246,18 @@ def test_invalid_arguments_do_not_create_capture(self): for invalid in ([], ["--sequence", "drain", "--labels", "0", "--mode", "each", "--precut", "1", "--tape-mm", "12"], ["--sequence", "drain", "--labels", "1", "--mode", "each", - "--precut", "1", "--tape-mm", "12", "--device", "usb://anything"]): + "--precut", "1", "--tape-mm", "12", "--device", "usb://anything"], + ["--sequence", "batch", "--labels", "3", "--mode", "end", + "--precut", "0", "--tape-mm", "12"], + ["--sequence", "batch", "--batches", "chain:3,chain:3,end:3", + "--precut", "1", "--tape-mm", "12"], + ["--sequence", "batch", "--batches", "none:2,", + "--precut", "1", "--tape-mm", "12"], + ["--sequence", "batch", "--labels", "1", "--mode", "end", + "--precut", "1", "--tape-mm", "12", "--continue", "2"], + ["--sequence", "batch", "--labels", "1", "--mode", "end", + "--precut", "1", "--tape-mm", "12", + "--device", "ptouch://Brother/PT-2730"]): result = subprocess.run([PROBE, "--capture", str(path)] + invalid, stdout=subprocess.PIPE, stderr=subprocess.PIPE) self.assertEqual(result.returncode, 2) diff --git a/tests/test_cutter.c b/tests/test_cutter.c index e904883..270ba91 100644 --- a/tests/test_cutter.c +++ b/tests/test_cutter.c @@ -1,101 +1,37 @@ #include #include "cutter.h" -int main(void) { - pt_op ops[64]; - - /* EACH, precut=1, n=3: per label PRINTINFO,SETMODE,SETADVANCED,PRINT, = 15 ops. - * labels 0,1 chain (0x00) + FF; final chains-off (0x08) + EJECT. */ - int n = pt_plan_batch(3, 12, PT_CUT_EACH, 1, false, ops, 64); - assert(n == 15); - assert(ops[0].kind == PT_OP_PRINTINFO && ops[0].arg == 12); - assert(ops[1].kind == PT_OP_SETMODE && ops[1].arg == 0x40); - assert(ops[2].kind == PT_OP_SETADVANCED && ops[2].arg == 0x00); - assert(ops[3].kind == PT_OP_PRINT_PAGE && ops[3].page_index == 0); - assert(ops[4].kind == PT_OP_FF); - /* final label (page 2) at ops[10..14] */ - assert(ops[12].kind == PT_OP_SETADVANCED && ops[12].arg == 0x08); - assert(ops[13].kind == PT_OP_PRINT_PAGE && ops[13].page_index == 2); - assert(ops[14].kind == PT_OP_EJECT); - - /* single label: precut + chain-off + eject (the "final-label combo") */ - n = pt_plan_batch(1, 12, PT_CUT_EACH, 1, false, ops, 64); - assert(n == 5); - assert(ops[0].kind == PT_OP_PRINTINFO); - assert(ops[1].kind == PT_OP_SETMODE && ops[1].arg == 0x40); - assert(ops[2].kind == PT_OP_SETADVANCED && ops[2].arg == 0x08); - assert(ops[4].kind == PT_OP_EJECT); - - /* With precut disabled, cut BEFORE copies 2/3 and AFTER the final one. - * Turning autocut off on the final copy left copies 2/3 joined on PT-2730. */ - n = pt_plan_batch(3, 24, PT_CUT_EACH, 0, false, ops, 64); - int modes[3] = {0}, found = 0; - for (int i = 0; i < n; i++) { - if (ops[i].kind == PT_OP_SETMODE) modes[found++] = ops[i].arg; +int main(void) +{ + const struct { + pt_cut_mode mode; + bool between, at_end; + } cases[] = { + {PT_CUT_CHAIN, true, false}, + {PT_CUT_EACH, true, true}, + {PT_CUT_NONE, false, false}, + {PT_CUT_END, false, true}, + }; + const int counts[] = {1, 3, 65, 999}; + + for (unsigned m = 0; m < sizeof cases / sizeof cases[0]; m++) { + pt_cut_policy policy = pt_cut_policy_for(cases[m].mode); + assert(policy.cut_between == cases[m].between); + assert(policy.cut_at_end == cases[m].at_end); + + for (unsigned c = 0; c < sizeof counts / sizeof counts[0]; c++) { + int autocuts = 0; + for (int label = 0; label < counts[c]; label++) { + bool autocut = pt_cut_autocut(policy, label == 0); + assert(autocut == (label == 0 || cases[m].between)); + autocuts += autocut; + } + /* Mandatory precut happens once, even for no-cut/final-only. */ + assert(autocuts == (cases[m].between ? counts[c] : 1)); + } + + /* The next batch starts with precut even on a healthy open run. */ + assert(pt_cut_autocut(policy, true)); } - assert(found == 3 && modes[0] == 0 && modes[1] == 0x40 && modes[2] == 0x40); - assert(ops[n - 1].kind == PT_OP_EJECT); - n = pt_plan_batch(1, 24, PT_CUT_EACH, 0, false, ops, 64); - assert(n == 4 && ops[0].kind == PT_OP_SETMODE && ops[0].arg == 0); - assert(ops[n - 1].kind == PT_OP_EJECT); - - /* NONE: no printinfo, setmode 0x00, chain 0x00, FF (no eject) — 4 ops/label */ - n = pt_plan_batch(2, 12, PT_CUT_NONE, 1, false, ops, 64); - assert(n == 8); - assert(ops[0].kind == PT_OP_SETMODE && ops[0].arg == 0x00); - assert(ops[1].kind == PT_OP_SETADVANCED && ops[1].arg == 0x00); - assert(ops[2].kind == PT_OP_PRINT_PAGE); - assert(ops[3].kind == PT_OP_FF); - assert(ops[7].kind == PT_OP_FF); - - /* cap too small -> -1 */ - assert(pt_plan_batch(3, 12, PT_CUT_EACH, 1, false, ops, 4) == -1); - - /* --- chain_out: another job follows, so the last label is not terminal (#41) --- */ - - /* EACH, n=1, precut=1, chained: autocut STAYS ON (the boundary between this - * label and the next job's must still be cut), chain 0x00, finish FF, no EJECT. */ - n = pt_plan_batch(1, 12, PT_CUT_EACH, 1, true, ops, 64); - assert(n == 5); - assert(ops[0].kind == PT_OP_PRINTINFO && ops[0].arg == 12); - assert(ops[1].kind == PT_OP_SETMODE && ops[1].arg == 0x40); - assert(ops[2].kind == PT_OP_SETADVANCED && ops[2].arg == 0x00); - assert(ops[3].kind == PT_OP_PRINT_PAGE); - assert(ops[4].kind == PT_OP_FF); - - /* EACH, n=3, chained: only the last label differs from the unchained plan. */ - n = pt_plan_batch(3, 12, PT_CUT_EACH, 1, true, ops, 64); - assert(n == 15); - assert(ops[11].kind == PT_OP_SETMODE && ops[11].arg == 0x40); - assert(ops[12].kind == PT_OP_SETADVANCED && ops[12].arg == 0x00); - assert(ops[14].kind == PT_OP_FF); - for (int i = 0; i < n; i++) - assert(ops[i].kind != PT_OP_EJECT); - - /* END + precut=1, chained: finish FF and chain 0x00, and autocut is left exactly - * as today's code produces it. do_precut already forces SETMODE 0x40 on every - * label in `end` mode (a pre-existing defect, #42) - assert that, not the - * aspiration, so this test cannot mask a chain-induced change. */ - n = pt_plan_batch(1, 12, PT_CUT_END, 1, true, ops, 64); - assert(n == 5); - assert(ops[0].kind == PT_OP_PRINTINFO); - assert(ops[1].kind == PT_OP_SETMODE && ops[1].arg == 0x40); - assert(ops[2].kind == PT_OP_SETADVANCED && ops[2].arg == 0x00); - assert(ops[4].kind == PT_OP_FF); - - /* END + precut=0, chained: no autocut at a job boundary, finish FF. */ - n = pt_plan_batch(1, 12, PT_CUT_END, 0, true, ops, 64); - assert(n == 4); - assert(ops[0].kind == PT_OP_SETMODE && ops[0].arg == 0x00); - assert(ops[1].kind == PT_OP_SETADVANCED && ops[1].arg == 0x00); - assert(ops[3].kind == PT_OP_FF); - - /* NONE ignores chain_out entirely. */ - pt_op none_ops[64]; - int n_none = pt_plan_batch(2, 12, PT_CUT_NONE, 1, false, none_ops, 64); - n = pt_plan_batch(2, 12, PT_CUT_NONE, 1, true, ops, 64); - assert(n == n_none); - for (int i = 0; i < n; i++) - assert(ops[i].kind == none_ops[i].kind && ops[i].arg == none_ops[i].arg); return 0; } diff --git a/tests/test_device_usb.c b/tests/test_device_usb.c new file mode 100644 index 0000000..e28abc6 --- /dev/null +++ b/tests/test_device_usb.c @@ -0,0 +1,200 @@ +/* Production USB selection and I/O boundary with fake enumeration only. + * No USB hardware, string descriptors, serial probes, or control transfers. */ +#include +#include +#include + +#define libusb_init fake_usb_init +#define libusb_exit fake_usb_exit +#define libusb_get_device_list fake_usb_get_device_list +#define libusb_free_device_list fake_usb_free_device_list +#define libusb_get_device_descriptor fake_usb_get_device_descriptor +#define libusb_open fake_usb_open +#define libusb_close fake_usb_close +#define libusb_kernel_driver_active fake_usb_kernel_driver_active +#define libusb_detach_kernel_driver fake_usb_detach_kernel_driver +#define libusb_attach_kernel_driver fake_usb_attach_kernel_driver +#define libusb_claim_interface fake_usb_claim_interface +#define libusb_release_interface fake_usb_release_interface +#define libusb_bulk_transfer fake_usb_bulk_transfer +#define papplDeviceGetData tracked_device_data +#include "../src/device_usb.c" +#undef papplDeviceGetData +extern void *papplDeviceGetData(pappl_device_t *device); + +static const char *uri = "ptouch://Brother/PT-2730"; +static struct { uint16_t vid, pid; int node; } devices[4]; +static libusb_device *device_list[5]; +static ssize_t num_devices; +static int contexts, init_calls, opens, claims, closes, transfers, data_reads; +static int selected_node, claim_result, enumeration_error; +static bool detached; +static int listed, errors; +static char last_error[256]; + +int fake_usb_init(libusb_context **ctx) +{ + contexts++; init_calls++; + *ctx = (libusb_context *)&contexts; + return 0; +} +void fake_usb_exit(libusb_context *ctx) { assert(ctx); contexts--; } +ssize_t fake_usb_get_device_list(libusb_context *ctx, libusb_device ***list) +{ + assert(ctx); + if (enumeration_error) return -1; + for (ssize_t i = 0; i < num_devices; i++) device_list[i] = (libusb_device *)&devices[i]; + device_list[num_devices] = NULL; + *list = device_list; + return num_devices; +} +void fake_usb_free_device_list(libusb_device **list, int unref) +{ assert(list == device_list && unref == 1); } +int fake_usb_get_device_descriptor(libusb_device *device, struct libusb_device_descriptor *d) +{ + for (ssize_t i = 0; i < num_devices; i++) if (device == (libusb_device *)&devices[i]) { + memset(d, 0, sizeof *d); d->idVendor = devices[i].vid; d->idProduct = devices[i].pid; + return 0; + } + assert(false); return -1; +} +int fake_usb_open(libusb_device *device, libusb_device_handle **handle) +{ + for (ssize_t i = 0; i < num_devices; i++) if (device == (libusb_device *)&devices[i]) { + opens++; selected_node = devices[i].node; + *handle = (libusb_device_handle *)device; + return 0; + } + assert(false); return -1; +} +void fake_usb_close(libusb_device_handle *h) { assert(h); closes++; } +int fake_usb_kernel_driver_active(libusb_device_handle *h, int interface) +{ assert(h && interface == 0); return 1; } +int fake_usb_detach_kernel_driver(libusb_device_handle *h, int interface) +{ assert(h && interface == 0); detached = true; return 0; } +int fake_usb_attach_kernel_driver(libusb_device_handle *h, int interface) +{ assert(h && interface == 0 && detached); detached = false; return 0; } +int fake_usb_claim_interface(libusb_device_handle *h, int interface) +{ assert(h && interface == 0); claims++; return claim_result; } +int fake_usb_release_interface(libusb_device_handle *h, int interface) +{ assert(h && interface == 0); return 0; } +int fake_usb_bulk_transfer(libusb_device_handle *h, unsigned char endpoint, + unsigned char *data, int size, int *transferred, unsigned int timeout) +{ + assert(h && size > 0); + assert(endpoint == PT_EP_IN || endpoint == PT_EP_OUT); + assert(timeout == PT_USB_TIMEOUT_MS || timeout == PT_USB_STATUS_TIMEOUT_MS); + transfers++; + if (endpoint == PT_EP_IN) memset(data, 0, (size_t)size); + *transferred = size; + return 0; +} +void *tracked_device_data(pappl_device_t *device) +{ data_reads++; return papplDeviceGetData(device); } + +static bool found(const char *info, const char *device_uri, const char *id, void *data) +{ + (void)data; + assert(!strcmp(info, "PT-2730 (USB)") && !strcmp(device_uri, uri)); + assert(strstr(id, "MDL:PT-2730;")); listed++; + return false; +} +static void error(const char *message, void *data) +{ (void)data; errors++; snprintf(last_error, sizeof last_error, "%s", message); } +static void inventory(int supported, bool unsupported) +{ + num_devices = supported + (unsupported ? 1 : 0); + for (int i = 0; i < supported; i++) { + devices[i].vid = 0x04f9; devices[i].pid = 0x2041; devices[i].node = i + 1; + } + if (unsupported) { + devices[supported].vid = 0x04f9; devices[supported].pid = 0x2061; + devices[supported].node = 99; + } + listed = errors = 0; last_error[0] = '\0'; +} + +int main(void) +{ + assert(pt_usb_supported_model(NULL) && pt_usb_supported_model("")); + assert(pt_usb_supported_model("PT-2730")); + assert(!pt_usb_supported_model("PT-P700") && !pt_usb_supported_model("typo")); + assert(pt_usb_is_uri(uri) && !pt_usb_is_uri("ptouch://Brother/")); + assert(!pt_usb_is_uri("ptouch://Brother/PT-P700") && !pt_usb_is_uri(NULL)); + pt_usb_register(); + + for (int count = 0; count <= 2; count++) { + inventory(count, false); + assert(pt_usb_count(0x04f9, 0x2041) == count); + assert(pt_usb_present(0x04f9, 0x2041) == (count == 1)); + assert(!pt_usb_list(found, NULL, error, NULL)); + assert(listed == (count == 1)); + if (count == 2) assert(errors && strstr(last_error, "ambiguous")); + int previous_opens = opens; + pappl_device_t *device = papplDeviceOpen(uri, "selection-test", error, NULL); + assert((device != NULL) == (count == 1)); + if (device) { + uint8_t bytes[32]; + assert(pt_usb_read_status(device, uri, bytes, sizeof bytes) == 32); + assert(opens == previous_opens + 1 && selected_node == 1); + papplDeviceClose(device); + } else assert(opens == previous_opens); + assert(contexts == 0 && !detached); + } + + inventory(1, true); + assert(!pt_usb_list(found, NULL, error, NULL)); + assert(listed == 1 && errors == 1 && strstr(last_error, "PT-P700")); + int previous_opens = opens, previous_inits = init_calls; + assert(!papplDeviceOpen("ptouch://Brother/PT-P700", "unsupported-test", error, NULL)); + assert(opens == previous_opens && init_calls == previous_inits); + inventory(0, true); + assert(!pt_usb_list(found, NULL, error, NULL) && !listed && errors); + assert(!papplDeviceOpen(uri, "unsupported-only-test", error, NULL)); + assert(opens == previous_opens); + + /* Replug changes both the node and enumeration position, never the URI. */ + inventory(1, true); + devices[0].pid = 0x2061; devices[0].node = 99; + devices[1].pid = 0x2041; devices[1].node = 42; + pappl_device_t *device = papplDeviceOpen(uri, "replug-test", error, NULL); + assert(device && selected_node == 42); papplDeviceClose(device); + inventory(0, false); + assert(!papplDeviceOpen(uri, "unplug-test", error, NULL)); + inventory(1, false); devices[0].node = 87; + device = papplDeviceOpen(uri, "replug-test", error, NULL); + assert(device && selected_node == 87); papplDeviceClose(device); + assert(contexts == 0 && !detached); + + claim_result = LIBUSB_ERROR_BUSY; + assert(!papplDeviceOpen(uri, "unclaimable-test", error, NULL)); + assert(contexts == 0 && !detached && closes == opens); + claim_result = 0; + enumeration_error = 1; + assert(pt_usb_count(0x04f9, 0x2041) == -1 && !pt_usb_present(0x04f9, 0x2041)); + assert(!papplDeviceOpen(uri, "enumeration-error-test", error, NULL)); + assert(contexts == 0); + enumeration_error = 0; + + /* Real PAPPL socket connection while a Brother is in the fake inventory. + * The guard must return before accessing opaque network connection data. */ + int listener = socket(AF_INET, SOCK_STREAM, 0); assert(listener >= 0); + struct sockaddr_in address = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; + assert(bind(listener, (struct sockaddr *)&address, sizeof address) == 0); + assert(listen(listener, 1) == 0); + socklen_t length = sizeof address; + assert(getsockname(listener, (struct sockaddr *)&address, &length) == 0); + char socket_uri[80]; + snprintf(socket_uri, sizeof socket_uri, "socket://127.0.0.1:%u", ntohs(address.sin_port)); + device = papplDeviceOpen(socket_uri, "socket-guard-test", error, NULL); assert(device); + int previous_reads = data_reads, previous_transfers = transfers; + uint8_t packet[32]; + assert(pt_usb_present(0x04f9, 0x2041)); + assert(pt_usb_read_status(device, socket_uri, packet, sizeof packet) == -1); + assert(pt_usb_read_status(device, NULL, packet, sizeof packet) == -1); + assert(data_reads == previous_reads && transfers == previous_transfers); + papplDeviceClose(device); close(listener); + assert(contexts == 0 && !detached && claims == opens); + puts("USB selection, model support, replug, and socket guard passed"); + return 0; +} diff --git a/tests/test_driver.c b/tests/test_driver.c index c656e61..6cc4421 100644 --- a/tests/test_driver.c +++ b/tests/test_driver.c @@ -4,18 +4,40 @@ #include #include #include +#include +#include #include "filter_png.h" #include "../src/driver.c" -static unsigned char wire[65536], statuses[128]; +static bool pt_media_confirmed(pappl_printer_t *printer) +{ + pt_printer_state *state = pt_state(printer); + pthread_mutex_lock(&state->lock); + bool confirmed = state->media_confirmed; + pthread_mutex_unlock(&state->lock); + return confirmed; +} + +static unsigned char wire[65536], statuses[160]; static size_t wire_used, status_used, status_size, status_chunk = 32; -static int fail_opcode = -1; -static bool usb_present = true; +static int fail_opcode = -1, fail_advanced = -1; +static bool counting; +static struct { size_t rows, ff, eject, modes, cuts, final, infos, inits; } counts; +static bool usb_present = true, can_open = true; +static ssize_t usb_matches = 1; +static unsigned usb_enumerations, usb_reads; +static pappl_printer_t *poll_during_job; +static bool busy_poll_checked; -bool pt_usb_present(uint16_t vid, uint16_t pid) { (void)vid; (void)pid; return usb_present; } -ssize_t pt_usb_read_status(pappl_device_t *d, void *buffer, size_t bytes) +ssize_t pt_usb_count(uint16_t vid, uint16_t pid) +{ (void)vid; (void)pid; usb_enumerations++; return usb_present ? usb_matches : 0; } +bool pt_usb_present(uint16_t vid, uint16_t pid) { return pt_usb_count(vid, pid) == 1; } +bool pt_usb_is_uri(const char *uri) +{ return uri && !strcmp(uri, "ptouch://Brother/PT-2730"); } +ssize_t pt_usb_read_status(pappl_device_t *d, const char *uri, void *buffer, size_t bytes) { - (void)d; + (void)d; (void)uri; + usb_reads++; size_t n = status_size - status_used; if (n > bytes) n = bytes; if (n > status_chunk) n = status_chunk; @@ -24,13 +46,38 @@ ssize_t pt_usb_read_status(pappl_device_t *d, void *buffer, size_t bytes) return (ssize_t)n; } static bool dev_open(pappl_device_t *d, const char *uri, const char *name) -{ (void)d; (void)uri; (void)name; return true; } +{ (void)d; (void)uri; (void)name; return can_open; } static void dev_close(pappl_device_t *d) { (void)d; } static ssize_t dev_write(pappl_device_t *d, const void *buffer, size_t n) { (void)d; const unsigned char *p = buffer; + if (n && p[0] == 'G' && poll_during_job && !busy_poll_checked) { + assert(papplPrinterGetState(poll_during_job) == IPP_PSTATE_PROCESSING); + assert(pt_media_confirmed(poll_during_job)); + pappl_preason_t before = papplPrinterGetReasons(poll_during_job); + unsigned reads = usb_reads; + assert(status_cb(poll_during_job)); + assert(pt_media_confirmed(poll_during_job)); + assert(papplPrinterGetReasons(poll_during_job) == before && usb_reads == reads); + busy_poll_checked = true; + } if (n && p[0] == fail_opcode) return -1; + if (n == 4 && p[0] == 27 && p[1] == 'i' && p[2] == 'K' && p[3] == fail_advanced) return -1; + if (counting) { + if (p[0] == 'G') counts.rows++; + else if (p[0] == 12) counts.ff++; + else if (p[0] == 26) counts.eject++; + else if (p[0] == 27 && p[1] == '@') counts.inits++; + else if (p[0] == 27 && p[1] == 'i' && p[2] == 'c') counts.infos++; + else if (p[0] == 27 && p[1] == 'i' && p[2] == 'M') { + counts.modes++; counts.cuts += p[3] == 0x40; + } else if (p[0] == 27 && p[1] == 'i' && p[2] == 'K' && p[3] == 8) { + counts.final++; + assert(counts.rows == counts.infos * 180); + } + return (ssize_t)n; + } assert(wire_used + n <= sizeof wire); memcpy(wire + wire_used, buffer, n); wire_used += n; @@ -40,7 +87,7 @@ static pappl_preason_t dev_status(pappl_device_t *d) { (void)d; return PAPPL_PRE static char *dev_id(pappl_device_t *d, char *buffer, size_t n) { (void)d; snprintf(buffer, n, "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;"); return buffer; } -static void reset_wire(void) { wire_used = 0; fail_opcode = -1; } +static void reset_wire(void) { wire_used = 0; fail_opcode = fail_advanced = -1; } static void ready_status(int error, size_t chunk) { memset(statuses, 0, sizeof statuses); @@ -66,6 +113,30 @@ static bool fixture_driver(pappl_system_t *s, const char *name, const char *uri, data->status_cb = fixed_status; /* IPP validation must not poll absent hardware. */ return ok; } +static void add_finishing(ipp_t *request, const char *finishing, bool collection) +{ + if (finishing) { + if (collection) { + ipp_t *col = ippNew(); + if (!strcmp(finishing, "none") || !strcmp(finishing, "{finishing-template=none}")) + ippAddOutOfBand(request, IPP_TAG_JOB, IPP_TAG_NOVALUE, "finishings-col"); + else { + const char *name = !strcmp(finishing, "{finishing-template=trim}") ? "trim" : finishing; + ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, name); + ippAddCollection(request, IPP_TAG_JOB, "finishings-col", col); + } + ippDelete(col); + } else if (!strcmp(finishing, "combined")) { + const int values[] = {3, 60, 63}; + ippAddIntegers(request, IPP_TAG_JOB, IPP_TAG_ENUM, "finishings", 3, values); + } else { + ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_ENUM, "finishings", + !strcmp(finishing, "trim") ? 11 : !strcmp(finishing, "trim-after-pages") ? 60 : + !strcmp(finishing, "trim-after-job") ? 63 : 3); + } + } +} + static pappl_job_t *job(pappl_printer_t *p, const char *finishing, int copies, bool collection) { char resource[128], uri[256]; @@ -76,18 +147,7 @@ static pappl_job_t *job(pappl_printer_t *p, const char *finishing, int copies, b ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, "test"); ippAddString(request, IPP_TAG_JOB, IPP_TAG_KEYWORD, "job-hold-until", NULL, "indefinite"); ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_INTEGER, "copies", copies); - if (finishing) { - if (collection) { - ipp_t *col = ippNew(); - ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, - strstr(finishing, "trim") ? "trim" : "none"); - ippAddCollection(request, IPP_TAG_JOB, "finishings-col", col); - ippDelete(col); - } else { - ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_ENUM, "finishings", - !strcmp(finishing, "trim") ? IPP_FINISHINGS_TRIM : IPP_FINISHINGS_NONE); - } - } + add_finishing(request, finishing, collection); ipp_t *response = cupsDoRequest(http, request, resource); if (!response || ippGetStatusCode(response) > IPP_STATUS_OK_EVENTS_COMPLETE) fprintf(stderr, "Create-Job failed: %s (%s)\n", cupsLastErrorString(), resource); @@ -106,16 +166,20 @@ static pappl_pr_options_t *begin(pappl_job_t *j, pappl_device_t *d) static bool page_test(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) { unsigned char pixels[32] = {0}; - return r_startpage(j, o, d, 0) && r_writeline(j, o, d, 0, pixels) && r_endpage(j, o, d, 0); + if (!r_startpage(j, o, d, 0) || !r_writeline(j, o, d, 0, pixels)) return false; + size_t before = wire_used; + bool result = r_endpage(j, o, d, 0); + assert(wire_used == before); /* Page completion cannot guess the batch end. */ + return result; } static void end(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) { - size_t before = wire_used; assert(r_endjob(j, o, d)); assert(!papplJobGetData(j)); + size_t before = wire_used; assert(r_endjob(j, o, d)); assert(wire_used == before); papplJobDeletePrintOptions(o); } -static void expect_wire(int inits, int rows, int ffs, int ejects, bool autocut) +static void expect_wire(int inits, int rows, int ffs, int ejects) { int actual_init = 0, actual_rows = 0, actual_ff = 0, actual_eject = 0; for (size_t pos = 0; pos < wire_used;) { @@ -125,7 +189,8 @@ static void expect_wire(int inits, int rows, int ffs, int ejects, bool autocut) else if (wire[pos] == 0x1b && wire[pos + 1] == 0x40) { pos += 2; actual_init++; } else { assert(wire[pos] == 0x1b && wire[pos + 1] == 0x69); - if (wire[pos + 2] == 'M' && !autocut) assert(wire[pos + 3] == 0); + if (wire[pos + 2] == 'M') + assert(wire[pos + 3] == 0x40 || wire[pos + 3] == 0); pos += wire[pos + 2] == 'c' ? 8 : 4; } assert(pos <= wire_used); @@ -136,6 +201,41 @@ static void expect_wire(int inits, int rows, int ffs, int ejects, bool autocut) assert(actual_init == inits && actual_rows == rows && actual_ff == ffs && actual_eject == ejects); } +/* Verify what the wire requests, including the first-only mandatory precut + * and late finalization after the last label's raster. */ +static void expect_policy(unsigned labels, const unsigned *lengths, pt_cut_mode mode) +{ + unsigned infos = 0, modes = 0, ff = 0, eject = 0, advanced_final = 0, rows = 0; + for (size_t pos = 0; pos < wire_used;) { + if (wire[pos] == 0x47) { rows++; pos += 3 + wire[pos + 1]; } + else if (wire[pos] == 0x0c) { ff++; pos++; } + else if (wire[pos] == 0x1a) { eject++; pos++; } + else if (wire[pos] == 0x1b && wire[pos + 1] == 0x40) pos += 2; + else { + assert(wire[pos] == 0x1b && wire[pos + 1] == 0x69); + unsigned char op = wire[pos + 2], arg = wire[pos + 3]; + if (op == 'c') { + if (infos) assert(rows == lengths[infos - 1]); + rows = 0; infos++; + } else if (op == 'M') { + bool cut = modes == 0 || mode == PT_CUT_EACH || mode == PT_CUT_CHAIN; + assert(arg == (cut ? 0x40 : 0)); modes++; + } else if (op == 'K') { + assert(arg == 0 || arg == 8); + if (arg == 8) { + assert(infos == labels && rows == lengths[labels - 1]); + advanced_final++; + } + } + pos += op == 'c' ? 8 : 4; + } + assert(pos <= wire_used); + } + assert(infos == labels && modes == labels && rows == lengths[labels - 1]); + bool final = mode == PT_CUT_EACH || mode == PT_CUT_END; + assert(ff == labels - (unsigned)final && eject == (unsigned)final && advanced_final == (unsigned)final); +} + static void *socket_sink(void *arg) { int listener = *(int *)arg; @@ -200,7 +300,252 @@ static void png_socket_case(pappl_printer_t *p, const char *finishing, int copie } assert(papplJobGetState(j) == (malformed ? IPP_JSTATE_ABORTED : IPP_JSTATE_COMPLETED)); pthread_join(sink, NULL); - expect_wire(inits, malformed ? 0 : 180 * copies, ffs, ejects, ejects != 0); + expect_wire(inits, malformed ? 0 : 180 * copies, ffs, ejects); +} + +static void raster_socket_case(pappl_printer_t *printer, const char *finishing, + bool collection, int pages, int copies, int bits, cups_mode_t mode, + int damage) +{ + const unsigned lengths[] = {180, 360, 90}; + char path[] = "/tmp/ptouch-batch-raster-XXXXXX"; + int fd = mkstemp(path); assert(fd >= 0); + cups_raster_t *raster = cupsRasterOpen(fd, mode); assert(raster); + for (int i = 0; i < pages; i++) { + cups_page_header2_t header; + pwg_media_t media = {"custom", NULL, NULL, 1200, (int)lengths[i] * 2540 / 180}; + assert(cupsRasterInitPWGHeader(&header, &media, bits == 1 ? "black_1" : "black_8", + 180, 180, "one-sided", "normal")); + if (mode == CUPS_RASTER_WRITE_APPLE) header.cupsColorSpace = CUPS_CSPACE_SW; + header.cupsInteger[CUPS_RASTER_PWG_TotalPageCount] = 0; + assert(header.cupsHeight == lengths[i] && cupsRasterWriteHeader2(raster, &header)); + unsigned char row[128]; assert(header.cupsBytesPerLine <= sizeof row); + memset(row, mode == CUPS_RASTER_WRITE_APPLE ? 255 : 0, sizeof row); + for (unsigned y = 0; y < header.cupsHeight; y++) { + if (damage == 2) row[0] = y == 0 || y == header.cupsHeight - 1 ? 128 : 0; + assert(cupsRasterWritePixels(raster, row, header.cupsBytesPerLine) == header.cupsBytesPerLine); + } + } + cupsRasterClose(raster); + if (damage == 1) { + assert(lseek(fd, 0, SEEK_END) >= 0); + assert(write(fd, "x", 1) == 1); + } else if (damage == 2) { + off_t size = lseek(fd, 0, SEEK_END); assert(size > 3); + assert(!ftruncate(fd, size - 3)); + } + close(fd); + char resource[128], uri[256]; + snprintf(resource, sizeof resource, "/ipp/print/%s", papplPrinterGetName(printer)); + snprintf(uri, sizeof uri, "ipp://localhost:%d%s", port, resource); + ipp_t *request = ippNewRequest(IPP_OP_PRINT_JOB); + ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri); + ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, "test"); + ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, + mode == CUPS_RASTER_WRITE_APPLE ? "image/urf" : "image/pwg-raster"); + ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_INTEGER, "copies", copies); + add_finishing(request, finishing, collection); + reset_wire(); + pthread_t sink; + if (copies == 1) assert(!pthread_create(&sink, NULL, socket_sink, &sink_listener)); + ipp_t *reply = cupsDoFileRequest(http, request, resource, path); unlink(path); + if (copies != 1) { + assert(reply && ippGetStatusCode(reply) == IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES); + assert(wire_used == 0 && pt_run_get(printer) == PT_RUN_CLOSED); + ippDelete(reply); + return; + } + if (!reply || ippGetStatusCode(reply) > IPP_STATUS_OK_EVENTS_COMPLETE) + fprintf(stderr, "Raster request rejected: %s\n", cupsLastErrorString()); + assert(reply && ippGetStatusCode(reply) <= IPP_STATUS_OK_EVENTS_COMPLETE); + int id = ippGetInteger(ippFindAttribute(reply, "job-id", IPP_TAG_INTEGER), 0); + ippDelete(reply); pthread_join(sink, NULL); + pappl_job_t *job = papplPrinterFindJob(printer, id); assert(job); + for (unsigned i = 0; i < 1000 && papplJobGetState(job) < IPP_JSTATE_CANCELED; i++) { + struct timespec delay = {0, 10000000L}; nanosleep(&delay, NULL); + } + bool aborted = damage != 0; + if (papplJobGetState(job) != (aborted ? IPP_JSTATE_ABORTED : IPP_JSTATE_COMPLETED)) + fprintf(stderr, "Raster %s pages%d bits%d mode%d: state%d message%s\n", + finishing, pages, bits, mode, papplJobGetState(job), papplJobGetMessage(job)); + assert(papplJobGetState(job) == (aborted ? IPP_JSTATE_ABORTED : IPP_JSTATE_COMPLETED)); + if (damage) { + expect_wire(1, damage == 1 ? 180 : 539, damage == 1 ? 0 : 1, 0); + assert(pt_run_get(printer) == PT_RUN_UNCERTAIN); + } else { + pt_cut_mode selected = !strcmp(finishing, "none") ? PT_CUT_NONE : + !strcmp(finishing, "ptouch-chain") ? PT_CUT_CHAIN : + !strcmp(finishing, "trim-after-job") ? PT_CUT_END : PT_CUT_EACH; + expect_policy((unsigned)pages, lengths, selected); + } +} + +static void counting_png_case(pappl_printer_t *printer, const char *finishing, + int copies, pt_cut_mode mode) +{ + pappl_job_t *j = upload_png(printer, finishing, copies, false); + memset(&counts, 0, sizeof counts); counting = true; + assert(papplJobRelease(j, "test")); + for (unsigned i = 0; i < 2000 && papplJobGetState(j) < IPP_JSTATE_CANCELED; i++) { + struct timespec delay = {0, 10000000L}; nanosleep(&delay, NULL); + } + assert(papplJobGetState(j) == IPP_JSTATE_COMPLETED); + counting = false; + bool between = mode == PT_CUT_EACH || mode == PT_CUT_CHAIN; + bool final = mode == PT_CUT_EACH || mode == PT_CUT_END; + assert(counts.rows == 180u * copies && counts.infos == (unsigned)copies && counts.modes == (unsigned)copies); + assert(counts.cuts == (between ? (unsigned)copies : 1)); + assert(counts.ff == (unsigned)copies - (unsigned)final && counts.eject == (unsigned)final && counts.final == (unsigned)final); +} + +static ipp_t *printer_request(pappl_printer_t *printer, ipp_op_t op) +{ + char uri[256]; + snprintf(uri, sizeof uri, "ipp://localhost:%d/ipp/print/%s", port, papplPrinterGetName(printer)); + ipp_t *request = ippNewRequest(op); + ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri); + ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, "test"); + return request; +} + +static ipp_t *printer_response(pappl_printer_t *printer, ipp_t *request) +{ + char resource[128]; snprintf(resource, sizeof resource, "/ipp/print/%s", papplPrinterGetName(printer)); + ipp_t *reply = cupsDoRequest(http, request, resource); assert(reply); return reply; +} + +static void check_capabilities(pappl_printer_t *printer) +{ + const char *formats[] = {"image/png", "image/pwg-raster", "image/urf"}; + for (unsigned f = 0; f < 3; f++) { + ipp_t *q = printer_request(printer, IPP_OP_GET_PRINTER_ATTRIBUTES); + ippAddString(q, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, formats[f]); + ipp_t *reply = printer_response(printer, q); + ipp_attribute_t *copies = ippFindAttribute(reply, "copies-supported", IPP_TAG_RANGE); + int high; assert(copies && ippGetRange(copies, 0, &high) == 1 && high == (f ? 1 : 999)); + ipp_attribute_t *supported = ippFindAttribute(reply, "finishings-supported", IPP_TAG_ENUM); + const int expected[] = {3, 11, 60, 63}; + assert(supported && ippGetCount(supported) == 4); + for (int i = 0; i < 4; i++) assert(ippContainsInteger(supported, expected[i])); + unsigned names = 0; + for (ipp_attribute_t *a = ippFirstAttribute(reply); a; a = ippNextAttribute(reply)) + if (ippGetName(a) && !strcmp(ippGetName(a), "finishings-default")) names++; + assert(names == 1); + ippDelete(reply); + } +} + +static void check_media_database(pappl_printer_t *printer) +{ + ipp_t *request = printer_request(printer, IPP_OP_GET_PRINTER_ATTRIBUTES); + const char *requested[] = {"media-col-database", "media-size-supported", "media-left-margin-supported", "media-right-margin-supported"}; + ippAddStrings(request, IPP_TAG_OPERATION, IPP_TAG_KEYWORD, "requested-attributes", 4, NULL, requested); + ipp_t *reply = printer_response(printer, request); + ipp_attribute_t *db = ippFindAttribute(reply, "media-col-database", IPP_TAG_BEGIN_COLLECTION); + ipp_attribute_t *sizes = ippFindAttribute(reply, "media-size-supported", IPP_TAG_BEGIN_COLLECTION); + assert(db && ippGetCount(db) == 20 && sizes && ippGetCount(sizes) == 5); + unsigned db_names = 0; + for (ipp_attribute_t *a = ippFirstAttribute(reply); a; a = ippNextAttribute(reply)) + if (ippGetName(a) && !strcmp(ippGetName(a), "media-col-database")) db_names++; + assert(db_names == 1); + const int widths[] = {6, 9, 12, 18, 24}; + for (unsigned w = 0; w < 5; w++) { + pt_geometry g; assert(pt_tape_geometry(widths[w], &g)); + unsigned fixed = 0, roll = 0; + for (int i = 0; i < ippGetCount(db); i++) { + ipp_t *col = ippGetCollection(db, i); + ipp_t *size = ippGetCollection(ippFindAttribute(col, "media-size", IPP_TAG_BEGIN_COLLECTION), 0); + ipp_attribute_t *x = ippFindAttribute(size, "x-dimension", IPP_TAG_ZERO); + if (!ippContainsInteger(x, widths[w] * 100)) continue; + assert(ippGetInteger(ippFindAttribute(col, "media-left-margin", IPP_TAG_INTEGER), 0) == g.left_margin_centimm); + assert(ippGetInteger(ippFindAttribute(col, "media-right-margin", IPP_TAG_INTEGER), 0) == g.right_margin_centimm); + ipp_attribute_t *length = ippFindAttribute(size, "y-dimension", IPP_TAG_ZERO); assert(length); + if (ippGetValueTag(length) == IPP_TAG_RANGE) { + int high; assert(ippGetRange(length, 0, &high) == PT_ROLL_MIN_CENTIMM && high == PT_ROLL_MAX_CENTIMM); roll++; + assert(ippGetValueTag(x) == IPP_TAG_RANGE); + assert(ippGetRange(x, 0, &high) == widths[w] * 100 - 1 && high == widths[w] * 100 + 1); + } else { + int value = ippGetInteger(length, 0); + assert(value == 2500 || value == 5000 || value == 10000); fixed++; + } + } + assert(fixed == 3 && roll == 1); + ipp_t *range = ippGetCollection(sizes, w); + int high; + assert(ippGetRange(ippFindAttribute(range, "x-dimension", IPP_TAG_RANGE), 0, &high) == widths[w] * 100 - 1 && high == widths[w] * 100 + 1); + assert(ippGetRange(ippFindAttribute(range, "y-dimension", IPP_TAG_RANGE), 0, &high) == PT_ROLL_MIN_CENTIMM && high == PT_ROLL_MAX_CENTIMM); + for (int delta = -2; delta <= 2; delta++) { + ipp_t *q = printer_request(printer, IPP_OP_VALIDATE_JOB); + ippAddBoolean(q, IPP_TAG_OPERATION, "ipp-attribute-fidelity", true); + ippAddString(q, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, "image/png"); + ipp_t *col = ippNew(), *size = ippNew(); + ippAddInteger(size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "x-dimension", widths[w] * 100 + delta); + ippAddInteger(size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "y-dimension", 4000); + ippAddCollection(col, IPP_TAG_ZERO, "media-size", size); + ippAddCollection(q, IPP_TAG_JOB, "media-col", col); ippDelete(size); ippDelete(col); + char resource[128]; snprintf(resource, sizeof resource, "/ipp/print/%s", papplPrinterGetName(printer)); + ipp_t *response = cupsDoRequest(http, q, resource); assert(response); + assert(ippGetStatusCode(response) == (abs(delta) <= 1 ? IPP_STATUS_OK : IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES)); + ippDelete(response); + } + } + ippDelete(reply); +} + +static void set_default(pappl_printer_t *printer, int value, const char *template) +{ + ipp_t *q = printer_request(printer, IPP_OP_SET_PRINTER_ATTRIBUTES); + if (template) { + ipp_t *col = ippNew(); + ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, template); + ippAddCollection(q, IPP_TAG_PRINTER, "finishings-col-default", col); ippDelete(col); + } else ippAddInteger(q, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-default", value); + ipp_t *reply = printer_response(printer, q); + assert(ippGetStatusCode(reply) <= IPP_STATUS_OK_EVENTS_COMPLETE); ippDelete(reply); + q = printer_request(printer, IPP_OP_GET_PRINTER_ATTRIBUTES); + reply = printer_response(printer, q); + ipp_attribute_t *def = ippFindAttribute(reply, "finishings-default", IPP_TAG_ENUM); + assert(def && ippGetInteger(def, 0) == value); + def = ippFindAttribute(reply, "finishings-col-default", IPP_TAG_ZERO); assert(def); + if (value == 3) assert(ippGetValueTag(def) == IPP_TAG_NOVALUE); + else { + const char *name = ippGetString(ippFindAttribute(ippGetCollection(def, 0), "finishing-template", IPP_TAG_KEYWORD), 0, NULL); + assert(name && !strcmp(name, template ? template : value == 11 ? "trim" : value == 60 ? "trim-after-pages" : "trim-after-job")); + } + ippDelete(reply); +} + +static void reject_finishing_requests(pappl_printer_t *printer) +{ + for (unsigned fidelity = 0; fidelity < 2; fidelity++) for (unsigned i = 0; i < 7; i++) { + ipp_t *q = printer_request(printer, i == 6 ? IPP_OP_SET_PRINTER_ATTRIBUTES : IPP_OP_CREATE_JOB); + ippAddBoolean(q, IPP_TAG_OPERATION, "ipp-attribute-fidelity", fidelity); + if (i == 0) { + add_finishing(q, "none", false); add_finishing(q, "trim", true); + } else if (i == 1) add_finishing(q, "unknown-template", true); + else if (i == 2) ippAddInteger(q, IPP_TAG_JOB, IPP_TAG_ENUM, "finishings", 62); + else if (i == 3) { + int values[] = {60, 62}; ippAddIntegers(q, IPP_TAG_JOB, IPP_TAG_ENUM, "finishings", 2, values); + } else if (i == 4) { + ipp_t *cols[] = {ippNew(), ippNew()}; + ippAddString(cols[0], IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, "trim-after-pages"); + ippAddString(cols[1], IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, "trim-after-job"); + ippAddCollections(q, IPP_TAG_JOB, "finishings-col", 2, (const ipp_t **)cols); + ippDelete(cols[0]); ippDelete(cols[1]); + } else if (i == 5) { + ipp_t *col = ippNew(); + ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, "trim"); + ippAddInteger(col, IPP_TAG_ZERO, IPP_TAG_ENUM, "trimming", 1); + ippAddCollection(q, IPP_TAG_JOB, "finishings-col", col); ippDelete(col); + } else { + ipp_t *col = ippNew(); + ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, "ptouch-chain"); + ippAddCollection(q, IPP_TAG_PRINTER, "finishings-col-default", col); ippDelete(col); + } + ipp_t *reply = printer_response(printer, q); + assert(ippGetStatusCode(reply) == IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES); + ippDelete(reply); + } } static void reject_unsupported_finishing(pappl_printer_t *p) @@ -222,28 +567,47 @@ int main(void) pappl_pr_driver_t driver = {"brother_ptouch", "Test", NULL, NULL}; papplDeviceAddScheme("fixture", PAPPL_DEVTYPE_CUSTOM_LOCAL, NULL, dev_open, dev_close, NULL, dev_write, dev_status, dev_id); + papplDeviceAddScheme("ptouch", PAPPL_DEVTYPE_CUSTOM_LOCAL, NULL, dev_open, dev_close, + NULL, dev_write, dev_status, dev_id); port = 20000 + getpid() % 20000; pappl_system_t *sys = papplSystemCreate(PAPPL_SOPTIONS_MULTI_QUEUE, "finishing-test", port, NULL, NULL, "/tmp/ptouch-driver-test.log", PAPPL_LOGLEVEL_ERROR, NULL, false); assert(sys); + /* Recognition metadata cannot select the PT-2730 protocol for another model. */ + const char *unsupported_ids[] = {"MFG:Brother;MDL:PT-P700;", "MFG:Brother;MDL:unknown;", ""}; + for (size_t i = 0; i < sizeof unsupported_ids / sizeof unsupported_ids[0]; i++) { + pappl_pr_driver_data_t rejected = {0}; + ipp_t *attrs = NULL; + assert(!pt_driver_cb(sys, "brother_ptouch", "socket://127.0.0.1:9", + unsupported_ids[i], &rejected, &attrs, NULL)); + assert(!rejected.rstartjob_cb && !rejected.printfile_cb && wire_used == 0); + } papplSystemSetPrinterDrivers(sys, 1, &driver, NULL, NULL, fixture_driver, NULL); pappl_printer_t *p = papplPrinterCreate(sys, 0, "test", "brother_ptouch", "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", "socket://127.0.0.1:9"); assert(p); - papplSystemAddMIMEFilter(sys, "image/png", PT_RASTER_FORMAT, pt_filter_png, NULL); + papplSystemAddMIMEFilter(sys, "image/png", "image/png", pt_filter_png, NULL); assert(papplSystemAddListeners(sys, "localhost")); pthread_t server; assert(!pthread_create(&server, NULL, run_server, sys)); http = httpConnect2("localhost", port, NULL, AF_UNSPEC, HTTP_ENCRYPTION_NEVER, 1, 3000, NULL); assert(http); pappl_device_t *d = papplDeviceOpen("fixture://test", "fixture", NULL, NULL); assert(d); + /* USB presence cannot change the transport selected for this printer. */ + reset_wire(); + unsigned enumeration_before = usb_enumerations, reads_before = usb_reads; + assert(usb_present && status_cb(p)); + pt_live_read incompatible = pt_read_live_tape(d, "socket://127.0.0.1:9"); + assert(incompatible.n < 0 && !incompatible.parsed); + assert(usb_enumerations == enumeration_before && usb_reads == reads_before && wire_used == 0); setenv("PTOUCH_CUT_MODE", "each", 1); /* Must not override an explicit none. */ - setenv("PTOUCH_PRECUT", "1", 1); + setenv("PTOUCH_PRECUT", "0", 1); /* Retired: mandatory precut cannot be disabled. */ const char *choices[] = {NULL, "none", "{finishing-template=none}"}; for (int i = 0; i < 3; i++) { - pt_chain_set(p, false); reset_wire(); + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); pappl_job_t *j = job(p, choices[i], 1, i == 2); - pappl_pr_options_t *o = begin(j, d); assert(o->finishings == PAPPL_FINISHINGS_NONE); + pappl_pr_options_t *o = begin(j, d); assert(page_test(j, o, d)); end(j, o, d); - expect_wire(1, 1, 1, 0, false); assert(pt_chain_get(p)); + expect_wire(1, 1, i ? 1 : 0, i ? 0 : 1); + assert(pt_chain_get(p) == (i != 0)); } /* A completed none job remains open across handle close/reopen. */ assert(pt_chain_get(p)); @@ -252,28 +616,65 @@ int main(void) pappl_job_t *trim = job(p, "trim", 3, false); pappl_pr_options_t *o = begin(trim, d); assert(o->finishings & PAPPL_FINISHINGS_TRIM); for (int i = 0; i < 3; i++) assert(page_test(trim, o, d)); - end(trim, o, d); expect_wire(0, 3, 2, 1, true); assert(!pt_chain_get(p)); + end(trim, o, d); expect_wire(0, 3, 2, 1); assert(!pt_chain_get(p)); reset_wire(); trim = job(p, "{finishing-template=trim}", 1, true); o = begin(trim, d); assert(o->finishings & PAPPL_FINISHINGS_TRIM); - assert(page_test(trim, o, d)); end(trim, o, d); expect_wire(1, 1, 0, 1, true); + assert(page_test(trim, o, d)); end(trim, o, d); expect_wire(1, 1, 0, 1); /* Buffered acceptance cannot hide failed raster or terminal transfers. */ const int failures[] = {0x47, 0x0c, 0x1a}; for (int i = 0; i < 3; i++) { - pt_chain_set(p, false); reset_wire(); + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); pappl_job_t *j = job(p, i == 2 ? "trim" : "none", 1, false); o = begin(j, d); assert(r_startpage(j, o, d, 0)); unsigned char pixels[32] = {0}; fail_opcode = failures[i]; - if (i == 0) assert(!r_writeline(j, o, d, 0, pixels)); - else { assert(r_writeline(j, o, d, 0, pixels)); assert(!r_endpage(j, o, d, 0)); } - assert(pt_chain_get(p)); end(j, o, d); + if (i == 0) { + assert(!r_writeline(j, o, d, 0, pixels)); + end(j, o, d); + } else { + assert(r_writeline(j, o, d, 0, pixels)); assert(r_endpage(j, o, d, 0)); + assert(!r_endjob(j, o, d) && !papplJobGetData(j)); + size_t failed_output = wire_used; + assert(r_endjob(j, o, d) && wire_used == failed_output); + papplJobDeletePrintOptions(o); + } + assert(pt_run_get(p) == PT_RUN_UNCERTAIN); + reset_wire(); + pappl_job_t *follower = job(p, "trim", 1, false); + pappl_pr_options_t *next = papplJobCreatePrintOptions(follower, 1, false); + assert(!r_startjob(follower, next, d) && wire_used == 0); + assert(status_cb(p) && pt_run_get(p) == PT_RUN_UNCERTAIN && wire_used == 0); + end(follower, next, d); } - pt_chain_set(p, false); reset_wire(); + /* Late chain-off is part of finishing; failure cannot proceed to EJECT. */ + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); + pappl_job_t *late_job = job(p, "trim", 1, false); + pappl_pr_options_t *late_options = begin(late_job, d); assert(page_test(late_job, late_options, d)); + size_t before_late = wire_used; fail_advanced = 8; + assert(!r_endjob(late_job, late_options, d) && wire_used == before_late); + assert(pt_run_get(p) == PT_RUN_UNCERTAIN); + assert(r_endjob(late_job, late_options, d) && wire_used == before_late); + papplJobDeletePrintOptions(late_options); + /* A failed intermediate FF does not configure or raster the next label. */ + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); + pappl_job_t *ff_job = job(p, "trim", 1, false); + pappl_pr_options_t *ff_options = begin(ff_job, d); assert(page_test(ff_job, ff_options, d)); + size_t before_ff = wire_used; fail_opcode = 12; + assert(!r_startpage(ff_job, ff_options, d, 1) && wire_used == before_ff); + assert(pt_run_get(p) == PT_RUN_UNCERTAIN); end(ff_job, ff_options, d); + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); pappl_job_t *j = job(p, "none", 1, false); o = begin(j, d); assert(r_startpage(j, o, d, 0)); papplJobCancel(j); - assert(!r_endpage(j, o, d, 0)); end(j, o, d); expect_wire(1, 0, 0, 0, false); + assert(!r_endpage(j, o, d, 0)); end(j, o, d); expect_wire(1, 0, 0, 0); + /* Cancellation after complete raster also leaves its print command pending. */ + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); + pappl_job_t *pending_cancel = job(p, "trim", 1, false); + pappl_pr_options_t *pending_options = begin(pending_cancel, d); assert(page_test(pending_cancel, pending_options, d)); + size_t pending_size = wire_used; papplJobCancel(pending_cancel); + end(pending_cancel, pending_options, d); + assert(wire_used == pending_size && pt_run_get(p) == PT_RUN_UNCERTAIN); /* Actual USB status parsing consumes all phases, including fragmented packets. */ pappl_printer_t *usb = papplPrinterCreate(sys, 0, "usb", "brother_ptouch", "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", "ptouch://Brother/PT-2730"); assert(usb); @@ -285,6 +686,25 @@ int main(void) ready_status(0, 32); statuses[0] = 0; assert(!pt_wait_ready(j, d)); papplJobCancel(j); ready_status(0, 32); assert(!pt_wait_ready(j, d)); assert(status_used == 0); + /* The deferred intermediate and final commands both consume complete + * receive-ready phases, including fragmented USB packets. */ + pt_run_set(usb, PT_RUN_CLOSED); reset_wire(); + ready_status(0, 7); statuses[18] = statuses[19] = 0; status_size = 32; + pappl_job_t *usb_batch = job(usb, "trim-after-job", 1, false); + pappl_pr_options_t *usb_options = begin(usb_batch, d); + assert(page_test(usb_batch, usb_options, d)); + ready_status(0, 7); assert(page_test(usb_batch, usb_options, d)); assert(status_used == status_size); + ready_status(0, 7); end(usb_batch, usb_options, d); assert(status_used == status_size); + assert(pt_run_get(usb) == PT_RUN_CLOSED); + /* A readiness error after the print command still blocks the next batch. */ + ready_status(0, 32); statuses[18] = statuses[19] = 0; status_size = 32; + usb_batch = job(usb, "none", 1, false); usb_options = begin(usb_batch, d); + assert(page_test(usb_batch, usb_options, d)); ready_status(4, 7); + assert(!r_endjob(usb_batch, usb_options, d) && pt_run_get(usb) == PT_RUN_UNCERTAIN); + size_t after_status_failure = wire_used; + assert(r_endjob(usb_batch, usb_options, d) && wire_used == after_status_failure); + papplJobDeletePrintOptions(usb_options); + pt_run_set(usb, PT_RUN_CLOSED); /* Explicit recovery simulated without hardware. */ /* Existing offline-media refresh still happens before geometry is built. */ usb_present = false; pt_chain_set(usb, true); assert(status_cb(usb)); assert(!pt_chain_get(usb) && !pt_media_confirmed(usb)); @@ -301,12 +721,118 @@ int main(void) assert(dd.media_default.size_width == 2400 && pt_media_confirmed(usb)); o = papplJobCreatePrintOptions(j, 1, false); assert(o->header.cupsWidth == 170); o->header.cupsWidth++; reset_wire(); - assert(!r_startjob(j, o, d)); assert(wire_used == 0); end(j, o, d); - assert(!pt_printfile_cb(j, NULL, d)); assert(wire_used == 0); + ready_status(0, 32); statuses[10] = 24; statuses[18] = statuses[19] = 0; status_size = 32; + assert(r_startjob(j, o, d)); reset_wire(); + assert(!r_startpage(j, o, d, 0)); assert(wire_used == 0); end(j, o, d); + assert(!pt_png_printfile(j, NULL, d)); assert(wire_used == 0); + + /* Every supported tape publishes a usable default and an exact window. */ + const int tapes[] = {6, 9, 12, 18, 24}; + for (unsigned i = 0; i < sizeof tapes / sizeof tapes[0]; i++) { + pt_geometry g; pappl_media_col_t media; + assert(pt_tape_geometry(tapes[i], &g) && media_col_for(tapes[i], 10000, 180, &media)); + assert(papplPrinterSetReadyMedia(p, 1, &media)); + pappl_job_t *geometry_job = job(p, "none", 1, false); + pappl_pr_options_t *geometry_options = papplJobCreatePrintOptions(geometry_job, 1, false); + int left = geometry_options->media.left_margin * 180 / 2540; + int span = (int)geometry_options->header.cupsWidth - + (geometry_options->media.left_margin + geometry_options->media.right_margin) * 180 / 2540; + assert(left == g.left_px && span == g.imageable_px); + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); + assert(r_startjob(geometry_job, geometry_options, d)); + geometry_options->header.cupsWidth++; + assert(!r_startpage(geometry_job, geometry_options, d, 0) && wire_used == 0); + assert(pt_run_get(p) == PT_RUN_CLOSED); + end(geometry_job, geometry_options, d); + } + pappl_media_col_t original_media; + assert(media_col_for(12, 10000, 180, &original_media)); + assert(papplPrinterSetReadyMedia(p, 1, &original_media)); + + /* Invalid/canceled followers before setup preserve a confirmed open run. */ + pt_run_set(p, PT_RUN_OPEN); reset_wire(); + pappl_job_t *invalid_follower = job(p, "trim", 1, false); + pappl_pr_options_t *invalid_options = papplJobCreatePrintOptions(invalid_follower, 1, false); + assert(r_startjob(invalid_follower, invalid_options, d)); + invalid_options->header.cupsWidth++; + assert(!r_startpage(invalid_follower, invalid_options, d, 0)); + assert(pt_run_get(p) == PT_RUN_OPEN && wire_used == 0); + end(invalid_follower, invalid_options, d); + pappl_job_t *canceled_follower = job(p, "trim", 1, false); + pappl_pr_options_t *canceled_options = papplJobCreatePrintOptions(canceled_follower, 1, false); + assert(r_startjob(canceled_follower, canceled_options, d)); + papplJobCancel(canceled_follower); + assert(!r_startpage(canceled_follower, canceled_options, d, 0)); + assert(pt_run_get(p) == PT_RUN_OPEN && wire_used == 0); + end(canceled_follower, canceled_options, d); + + pt_run_set(p, PT_RUN_CLOSED); reset_wire(); + pappl_job_t *bad_next = job(p, "trim", 1, false); + pappl_pr_options_t *bad_options = begin(bad_next, d); assert(page_test(bad_next, bad_options, d)); + size_t before_bad = wire_used; bad_options->header.cupsWidth++; + assert(!r_startpage(bad_next, bad_options, d, 1) && wire_used == before_bad); + end(bad_next, bad_options, d); assert(wire_used == before_bad && pt_run_get(p) == PT_RUN_UNCERTAIN); + /* Fault publication cannot turn present hardware into a ready printer. */ + usb_matches = 2; reset_wire(); assert(status_cb(usb)); + assert(wire_used == 0 && (papplPrinterGetReasons(usb) & PAPPL_PREASON_OFFLINE)); + usb_matches = 1; can_open = false; assert(status_cb(usb)); can_open = true; + assert(!pt_media_confirmed(usb) && (papplPrinterGetReasons(usb) & PAPPL_PREASON_OFFLINE)); + /* A real job owns the device when a status poll races its scheduler. + * OpenDevice returns NULL while processing, without a USB claim failure. */ + pappl_pr_driver_data_t before_busy; papplPrinterGetDriverData(usb, &before_busy); + ready_status(0, 32); statuses[18] = statuses[19] = 0; status_size = 32; + assert(status_cb(usb) && pt_media_confirmed(usb)); + ready_status(0, 32); + memmove(statuses + 64, statuses, 96); + memcpy(statuses + 32, statuses, 32); + statuses[18] = statuses[19] = 0; + statuses[32 + 18] = statuses[32 + 19] = 0; + status_size = 160; reset_wire(); pt_run_set(usb, PT_RUN_CLOSED); + pappl_job_t *busy_job = upload_png(usb, "none", 1, false); + poll_during_job = usb; busy_poll_checked = false; + assert(papplJobRelease(busy_job, "test")); + for (int i = 0; i < 1000 && papplJobGetState(busy_job) < IPP_JSTATE_CANCELED; i++) { + struct timespec delay = {0, 10000000L}; nanosleep(&delay, NULL); + } + poll_during_job = NULL; + assert(busy_poll_checked && papplJobGetState(busy_job) == IPP_JSTATE_COMPLETED); + assert(pt_media_confirmed(usb) && !(papplPrinterGetReasons(usb) & PAPPL_PREASON_OFFLINE)); + assert(papplPrinterSetReadyMedia(usb, 1, before_busy.media_ready)); + pt_run_set(usb, PT_RUN_CLOSED); + const int fault_tapes[] = {0, 36, 12, 12}; + for (unsigned i = 0; i < sizeof fault_tapes / sizeof fault_tapes[0]; i++) { + ready_status(i == 2 ? 4 : 0, 32); + statuses[10] = (unsigned char)fault_tapes[i]; statuses[18] = statuses[19] = 0; + if (i == 2) statuses[8] = 4; + if (i == 3) statuses[0] = 0; + status_size = 32; reset_wire(); + assert(status_cb(usb) && !pt_media_confirmed(usb)); + assert(papplPrinterGetReasons(usb) & (PAPPL_PREASON_MEDIA_EMPTY | PAPPL_PREASON_MEDIA_NEEDED | PAPPL_PREASON_OTHER | PAPPL_PREASON_OFFLINE)); + ready_status(i == 2 ? 4 : 0, 32); + statuses[10] = (unsigned char)fault_tapes[i]; statuses[18] = statuses[19] = 0; + if (i == 2) statuses[8] = 4; + if (i == 3) statuses[0] = 0; + status_size = 32; + pappl_job_t *fault_job = job(usb, "none", 1, false); + pappl_pr_options_t *fault_options = papplJobCreatePrintOptions(fault_job, 1, false); + reset_wire(); assert(!r_startjob(fault_job, fault_options, d)); + assert(wire_used == 3 && !memcmp(wire, "\x1b\x69\x53", 3)); + end(fault_job, fault_options, d); + } + /* Neither clean status nor reopening clears an uncertain output latch. */ + pt_run_set(usb, PT_RUN_UNCERTAIN); ready_status(0, 32); reset_wire(); + assert(status_cb(usb) && wire_used == 0 && status_used == 0); + pappl_job_t *blocked = job(usb, "trim", 1, false); + pappl_pr_options_t *blocked_options = papplJobCreatePrintOptions(blocked, 1, false); + assert(!r_startjob(blocked, blocked_options, d) && wire_used == 0); + assert(pt_run_get(usb) == PT_RUN_UNCERTAIN); + end(blocked, blocked_options, d); + /* A fresh server/fixture state after the operator's power-cycle. */ + pt_run_set(usb, PT_RUN_CLOSED); /* Cassette detection must survive an open none run without ESC @. */ pappl_printer_t *poll = papplPrinterCreate(sys, 0, "poll", "brother_ptouch", - "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", "fixture://poll"); assert(poll); + "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", "ptouch://Brother/PT-2730"); assert(poll); const int widths[] = {12, 24, 0}; for (int i = 0; i < 3; i++) { ready_status(0, 32); statuses[10] = widths[i]; statuses[18] = statuses[19] = 0; @@ -332,12 +858,14 @@ int main(void) o = papplJobCreatePrintOptions(j, 1, false); assert(o->header.cupsWidth == 85); papplJobDeletePrintOptions(o); - /* State exhaustion is a failure before output, never an implicit cut. */ - memset(g_chain, 0, sizeof g_chain); - for (int i = 0; i < PT_CHAIN_MAX; i++) g_chain[i].printer_id = 1000 + i; - reset_wire(); j = job(p, "none", 1, false); o = papplJobCreatePrintOptions(j, 1, false); - assert(!r_startjob(j, o, d)); assert(wire_used == 0); end(j, o, d); - memset(g_chain, 0, sizeof g_chain); + /* State belongs to each queue, with no arbitrary eight-printer ceiling. */ + for (int i = 0; i < 10; i++) { + char name[32]; snprintf(name, sizeof name, "owned-%d", i); + pappl_printer_t *owned = papplPrinterCreate(sys, 0, name, "brother_ptouch", + "MFG:Brother;MDL:PT-2730;", "socket://127.0.0.1:9"); + assert(owned && pt_state(owned) && pt_state(owned) != pt_state(p)); + assert(pt_run_get(owned) == PT_RUN_CLOSED && !pt_media_confirmed(owned)); + } /* PNG rendering and actual IPP enum/collection resolution on a TCP sink. */ sink_listener = socket(AF_INET, SOCK_STREAM, 0); assert(sink_listener >= 0); struct sockaddr_in address = { .sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK) }; @@ -348,8 +876,8 @@ int main(void) char socket_uri[128]; snprintf(socket_uri, sizeof socket_uri, "socket://127.0.0.1:%u", ntohs(address.sin_port)); pappl_printer_t *capture = papplPrinterCreate(sys, 0, "capture", "brother_ptouch", "MFG:Brother;MDL:PT-2730;CMD:PT-CBP;", socket_uri); assert(capture); - png_socket_case(capture, NULL, 1, false, 1, 1, 0); - png_socket_case(capture, "none", 3, false, 0, 3, 0); + png_socket_case(capture, NULL, 1, false, 1, 0, 1); + png_socket_case(capture, "none", 3, false, 1, 3, 0); png_socket_case(capture, "malformed", 1, false, 0, 0, 0); assert(pt_chain_get(capture)); pappl_job_t *canceled = job(capture, "trim", 1, false); papplJobCancel(canceled); @@ -374,9 +902,74 @@ int main(void) } assert(papplJobGetState(first) == IPP_JSTATE_COMPLETED); assert(papplJobGetState(follower) == IPP_JSTATE_COMPLETED); - pthread_join(queued_sink, NULL); expect_wire(2, 360, 0, 2, true); + pthread_join(queued_sink, NULL); expect_wire(2, 360, 0, 2); + const char *presets[] = {"ptouch-chain", "trim-after-pages", "none", "trim-after-job"}; + const pt_cut_mode modes[] = {PT_CUT_CHAIN, PT_CUT_EACH, PT_CUT_NONE, PT_CUT_END}; + check_capabilities(capture); + check_media_database(capture); + reject_finishing_requests(capture); + ipp_t *unchanged_default = printer_response(capture, printer_request(capture, IPP_OP_GET_PRINTER_ATTRIBUTES)); + assert(ippGetInteger(ippFindAttribute(unchanged_default, "finishings-default", IPP_TAG_ENUM), 0) == 11); + ippDelete(unchanged_default); + for (unsigned i = 0; i < 4; i++) { + for (int copies = 1; copies <= 3; copies += 2) { + pt_run_set(capture, PT_RUN_CLOSED); + bool final = i == 1 || i == 3; + png_socket_case(capture, presets[i], copies, i == 0, 1, + copies - (int)final, (int)final); + const unsigned lengths[] = {180, 180, 180}; + expect_policy((unsigned)copies, lengths, modes[i]); + } + for (int pages = 1; pages <= 3; pages++) { + pt_run_set(capture, PT_RUN_CLOSED); + raster_socket_case(capture, presets[i], i == 0, pages, 1, 1, CUPS_RASTER_WRITE_PWG, false); + } + pt_run_set(capture, PT_RUN_CLOSED); + raster_socket_case(capture, presets[i], i == 0, 3, 1, 8, CUPS_RASTER_WRITE_PWG, false); + pt_run_set(capture, PT_RUN_CLOSED); + raster_socket_case(capture, presets[i], i == 0, 3, 1, 8, CUPS_RASTER_WRITE_APPLE, false); + pt_run_set(capture, PT_RUN_CLOSED); + raster_socket_case(capture, presets[i], i == 0, 2, 1, 1, CUPS_RASTER_WRITE_PWG, 2); + pt_run_set(capture, PT_RUN_CLOSED); /* Simulate explicit recovery after partial output. */ + /* Registered collection templates must execute the same policy. */ + if (i != 0) { + pt_run_set(capture, PT_RUN_CLOSED); + png_socket_case(capture, presets[i], 3, true, 1, 3 - (int)(i != 2), i != 2); + const unsigned lengths[] = {180, 180, 180}; expect_policy(3, lengths, modes[i]); + } + } + pt_run_set(capture, PT_RUN_CLOSED); + png_socket_case(capture, "combined", 3, false, 1, 2, 1); + const unsigned combined_lengths[] = {180, 180, 180}; expect_policy(3, combined_lengths, PT_CUT_EACH); + /* Every preset transition: final modes reset once; continuing modes do not. */ + for (unsigned from = 0; from < 4; from++) for (unsigned to = 0; to < 4; to++) { + pt_run_set(capture, PT_RUN_CLOSED); + png_socket_case(capture, presets[from], 1, from == 0, 1, from == 0 || from == 2, from == 1 || from == 3); + png_socket_case(capture, presets[to], 1, to == 0, from == 1 || from == 3, to == 0 || to == 2, to == 1 || to == 3); + const unsigned length[] = {180}; expect_policy(1, length, modes[to]); + } + set_default(capture, 63, "trim-after-job"); pt_run_set(capture, PT_RUN_CLOSED); + png_socket_case(capture, NULL, 3, false, 1, 2, 1); + const unsigned defaults_lengths[] = {180, 180, 180}; expect_policy(3, defaults_lengths, PT_CUT_END); + set_default(capture, 3, NULL); pt_run_set(capture, PT_RUN_CLOSED); + png_socket_case(capture, NULL, 3, false, 1, 3, 0); expect_policy(3, defaults_lengths, PT_CUT_NONE); + set_default(capture, 60, NULL); pt_run_set(capture, PT_RUN_CLOSED); + png_socket_case(capture, NULL, 3, false, 1, 2, 1); expect_policy(3, defaults_lengths, PT_CUT_EACH); + set_default(capture, 11, NULL); + pt_run_set(capture, PT_RUN_CLOSED); + raster_socket_case(capture, "trim", false, 1, 2, 1, CUPS_RASTER_WRITE_PWG, false); + /* Malformed EOF cannot release the pending complete first label. */ + pt_run_set(capture, PT_RUN_CLOSED); + raster_socket_case(capture, "trim", false, 1, 1, 1, CUPS_RASTER_WRITE_PWG, true); + pt_run_set(capture, PT_RUN_CLOSED); /* Simulate documented explicit recovery. */ + pappl_printer_t *counter = papplPrinterCreate(sys, 0, "counter", "brother_ptouch", + "MFG:Brother;MDL:PT-2730;", "fixture://counter"); assert(counter); + counting_png_case(counter, "none", 65, PT_CUT_NONE); + counting_png_case(counter, "trim", 999, PT_CUT_EACH); close(sink_listener); papplDeviceClose(d); httpClose(http); papplSystemShutdown(sys); + /* Let the listener poll finish: opening a wake connection creates a detached + * PAPPL client that can still access sys after the server thread exits. */ pthread_join(server, NULL); papplSystemDelete(sys); puts("driver finishing and transfer checks passed"); return 0; diff --git a/tests/test_pappl_finishings.c b/tests/test_pappl_finishings.c new file mode 100644 index 0000000..1e9361c --- /dev/null +++ b/tests/test_pappl_finishings.c @@ -0,0 +1,211 @@ +/* Real IPP boundary tests, independent of Brother command policy. */ +#include +#include +#include +#include +#include +#include +#include +#include + +static pappl_system_t *fixture_system; +static pappl_printer_t *printer; +static http_t *http; +static int port; +static bool start(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) +{ (void)j; (void)o; (void)d; return true; } +static bool page(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d, unsigned p) +{ (void)j; (void)o; (void)d; (void)p; return true; } +static bool line(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d, unsigned y, const unsigned char *b) +{ (void)j; (void)o; (void)d; (void)y; (void)b; return true; } +static bool status(pappl_printer_t *p) { (void)p; return true; } +static ipp_t *collection(const char *name) +{ + ipp_t *col = ippNew(); + ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "finishing-template", NULL, name); + return col; +} +static bool driver(pappl_system_t *s, const char *n, const char *u, const char *id, + pappl_pr_driver_data_t *d, ipp_t **attrs, void *v) +{ + (void)s; (void)u; (void)id; (void)v; + strcpy(d->make_and_model, "Finishing bridge fixture"); d->kind = PAPPL_KIND_LABEL; d->ppm = 20; + d->rstartjob_cb = start; d->rstartpage_cb = page; d->rwriteline_cb = line; + d->rendpage_cb = page; d->rendjob_cb = start; d->status_cb = status; + d->finishings = PAPPL_FINISHINGS_TRIM; + d->color_supported = PAPPL_COLOR_MODE_BI_LEVEL | PAPPL_COLOR_MODE_MONOCHROME; + d->color_default = PAPPL_COLOR_MODE_BI_LEVEL; + d->raster_types = PAPPL_PWG_RASTER_TYPE_BLACK_1 | PAPPL_PWG_RASTER_TYPE_BLACK_8; + d->force_raster_type = PAPPL_PWG_RASTER_TYPE_BLACK_1; + d->num_resolution = 1; d->x_resolution[0] = d->y_resolution[0] = 180; + d->x_default = d->y_default = 180; + d->num_source = 1; d->source[0] = "main-roll"; d->num_type = 1; d->type[0] = "labels"; + d->num_media = 3; d->media[0] = "custom_12x100mm_12x100mm"; + d->media[1] = "roll_min_12x5mm"; d->media[2] = "roll_max_12x300mm"; + strcpy(d->media_default.size_name, d->media[0]); strcpy(d->media_default.source, "main-roll"); + strcpy(d->media_default.type, "labels"); d->media_default.tracking = PAPPL_MEDIA_TRACKING_CONTINUOUS; + d->media_default.size_width = 1200; d->media_default.size_length = 10000; + d->media_ready[0] = d->media_default; + if (!strcmp(n, "legacy")) return true; + *attrs = ippNew(); + int enums[] = {3, 11, 60, 63}; + const char *names[] = {"trim", "trim-after-pages", "trim-after-job", "ptouch-chain"}; + ipp_t *cols[4]; + for (int i = 0; i < 4; i++) cols[i] = collection(names[i]); + ippAddIntegers(*attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-supported", 4, enums); + ippAddIntegers(*attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-ready", 4, enums); + ippAddCollections(*attrs, IPP_TAG_PRINTER, "finishings-col-database", 4, (const ipp_t **)cols); + ippAddCollections(*attrs, IPP_TAG_PRINTER, "finishings-col-ready", 4, (const ipp_t **)cols); + ippAddStrings(*attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "finishing-template-supported", 4, NULL, names); + ippAddString(*attrs, IPP_TAG_PRINTER, IPP_TAG_KEYWORD, "finishings-col-supported", NULL, "finishing-template"); + ippAddInteger(*attrs, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-default", 11); + ippAddCollection(*attrs, IPP_TAG_PRINTER, "finishings-col-default", cols[0]); + for (int i = 0; i < 4; i++) ippDelete(cols[i]); + return true; +} +static void *run(void *s) { papplSystemRun(s); return NULL; } +static ipp_t *request(ipp_op_t op, const char *name) +{ + ipp_t *q = ippNewRequest(op); char uri[256]; + snprintf(uri, sizeof uri, "ipp://localhost:%d/ipp/print/%s", port, name); + ippAddString(q, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri); + ippAddString(q, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, "finishing-test"); + return q; +} +static ipp_t *submit(ipp_t *q, const char *name, bool success) +{ + char resource[128]; snprintf(resource, sizeof resource, "/ipp/print/%s", name); + ipp_t *r = cupsDoRequest(http, q, resource); assert(r); + fprintf(stderr, "%s: status=%s\n", name, ippErrorString(ippGetStatusCode(r))); + assert(success ? ippGetStatusCode(r) <= IPP_STATUS_OK_EVENTS_COMPLETE : ippGetStatusCode(r) == IPP_STATUS_ERROR_ATTRIBUTES_OR_VALUES); + return r; +} +static unsigned named_count(ipp_t *attrs, const char *name) +{ + unsigned count = 0; + for (ipp_attribute_t *a = ippFirstAttribute(attrs); a; a = ippNextAttribute(attrs)) + if (ippGetName(a) && !strcmp(ippGetName(a), name)) count++; + return count; +} +static void defaults(int value, const char *name) +{ + ipp_t *r = submit(request(IPP_OP_GET_PRINTER_ATTRIBUTES, "test"), "test", true); + assert(named_count(r, "finishings-supported") == 1); + assert(named_count(r, "finishings-col-database") == 1); + assert(named_count(r, "finishings-default") == 1); + assert(named_count(r, "finishings-col-default") == 1); + assert(!ippFindAttribute(r, "finishing-col-database", IPP_TAG_ZERO)); + assert(ippGetInteger(ippFindAttribute(r, "finishings-default", IPP_TAG_ENUM), 0) == value); + ipp_attribute_t *col = ippFindAttribute(r, "finishings-col-default", IPP_TAG_ZERO); assert(col); + if (!name) assert(ippGetValueTag(col) == IPP_TAG_NOVALUE); + else assert(!strcmp(ippGetString(ippFindAttribute(ippGetCollection(col, 0), "finishing-template", IPP_TAG_KEYWORD), 0, NULL), name)); + ippDelete(r); +} +static void job(const int *enums, int count, const char *template, bool no_value, bool success, const char *queue) +{ + ipp_t *q = request(IPP_OP_CREATE_JOB, queue); + ippAddString(q, IPP_TAG_JOB, IPP_TAG_KEYWORD, "job-hold-until", NULL, "indefinite"); + if (count) ippAddIntegers(q, IPP_TAG_JOB, IPP_TAG_ENUM, "finishings", count, enums); + if (template) { ipp_t *col = collection(template); ippAddCollection(q, IPP_TAG_JOB, "finishings-col", col); ippDelete(col); } + if (no_value) ippAddOutOfBand(q, IPP_TAG_JOB, IPP_TAG_NOVALUE, "finishings-col"); + ipp_t *r = submit(q, queue, success); + if (success) { + pappl_printer_t *p = !strcmp(queue, "test") ? printer : papplSystemFindPrinter(fixture_system, !strcmp(queue,"legacy") ? "/ipp/print/legacy" : "/ipp/print/test", 0, NULL); + pappl_job_t *j = papplPrinterFindJob(p, ippGetInteger(ippFindAttribute(r, "job-id", IPP_TAG_INTEGER), 0)); assert(j); + if (count) { ipp_attribute_t *a = papplJobGetAttribute(j, "finishings"); assert(ippGetCount(a) == count); for (int i=0;ifinishings == PAPPL_FINISHINGS_NONE); + papplJobDeletePrintOptions(options); papplJobCancel(j); + } + ippDelete(r); +} +static void update(const int *enums, int count, const char *name, bool no_value, bool success) +{ + ipp_t *q = request(IPP_OP_SET_PRINTER_ATTRIBUTES, "test"); + if (count) ippAddIntegers(q, IPP_TAG_PRINTER, IPP_TAG_ENUM, "finishings-default", count, enums); + if (name) { ipp_t *col = collection(name); ippAddCollection(q, IPP_TAG_PRINTER, "finishings-col-default", col); ippDelete(col); } + if (no_value) ippAddOutOfBand(q, IPP_TAG_PRINTER, IPP_TAG_NOVALUE, "finishings-col-default"); + ippDelete(submit(q, "test", success)); +} +static void setter(const char *value, bool success) +{ + pappl_pr_driver_data_t data, before, after; + papplPrinterGetDriverData(printer, &before); data = before; + data.orient_default = IPP_ORIENT_LANDSCAPE; + cups_option_t *options = NULL; + int count = value ? cupsAddOption("finishings", value, 0, &options) : 0; + assert(papplPrinterSetDriverDefaults(printer, &data, count, options) == success); + cupsFreeOptions(count, options); + papplPrinterGetDriverData(printer, &after); + if (!success) assert(after.orient_default == before.orient_default); +} +static void roundtrip(const char *directory, int value, const char *name) +{ + char state[512]; snprintf(state, sizeof state, "%s/state", directory); + assert(papplSystemSaveState(fixture_system, state)); + pappl_system_t *loaded = papplSystemCreate(PAPPL_SOPTIONS_MULTI_QUEUE, "loaded", port+1, NULL, directory, "-", PAPPL_LOGLEVEL_ERROR, NULL, false); assert(loaded); + pappl_pr_driver_t drivers[] = {{"preset", "Preset", NULL, NULL}, {"legacy", "Legacy", NULL, NULL}}; + papplSystemSetPrinterDrivers(loaded, 2, drivers, NULL, NULL, driver, NULL); + assert(papplSystemLoadState(loaded, state)); + pappl_printer_t *p = papplSystemFindPrinter(loaded, "/ipp/print/test", 0, NULL); assert(p); + ipp_t *attrs = papplPrinterGetDriverAttributes(p); assert(attrs); + assert(ippGetInteger(ippFindAttribute(attrs, "finishings-default", IPP_TAG_ENUM),0) == value); + ipp_attribute_t *col = ippFindAttribute(attrs, "finishings-col-default", IPP_TAG_ZERO); assert(col); + if (!name) assert(ippGetValueTag(col) == IPP_TAG_NOVALUE); + else assert(!strcmp(ippGetString(ippFindAttribute(ippGetCollection(col,0), "finishing-template", IPP_TAG_KEYWORD),0,NULL), name)); + assert(named_count(attrs,"finishings-default") == 1 && named_count(attrs,"finishings-col-default") == 1); + ippDelete(attrs); papplSystemDelete(loaded); unlink(state); +} +static void copies(const char *format, int number, bool success) +{ + ipp_t *q = request(IPP_OP_VALIDATE_JOB, "test"); + if (format) ippAddString(q, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, format); + ippAddInteger(q, IPP_TAG_JOB, IPP_TAG_INTEGER, "copies", number); + ippDelete(submit(q, "test", success)); +} +int main(void) +{ + char temp[] = "/tmp/pappl-finishings-XXXXXX"; assert(mkdtemp(temp)); + port = 22000 + getpid() % 15000; + fixture_system = papplSystemCreate(PAPPL_SOPTIONS_MULTI_QUEUE, "finishings-test", port, NULL, temp, "-", PAPPL_LOGLEVEL_ERROR, NULL, false); assert(fixture_system); + pappl_pr_driver_t drivers[] = {{"preset", "Preset", NULL, NULL}, {"legacy", "Legacy", NULL, NULL}}; + papplSystemSetPrinterDrivers(fixture_system, 2, drivers, NULL, NULL, driver, NULL); + printer = papplPrinterCreate(fixture_system, 0, "test", "preset", "MFG:Test;MDL:Fixture;", "socket://127.0.0.1:9"); assert(printer); + assert(papplPrinterCreate(fixture_system, 0, "legacy", "legacy", "MFG:Test;MDL:Fixture;", "socket://127.0.0.1:9")); + assert(papplSystemAddListeners(fixture_system, "localhost")); pthread_t server; assert(!pthread_create(&server, NULL, run, fixture_system)); + http = httpConnect2("localhost", port, NULL, AF_UNSPEC, HTTP_ENCRYPTION_NEVER, 1, 3000, NULL); assert(http); + defaults(11, "trim"); + const int one[] = {3, 11, 60, 63}, redundant[] = {60,63}, mixed[] = {3,60}, aliases[] = {11,60}, bad[] = {4}; + for (int i=0;i<4;i++) job(one+i, 1, NULL, false, true, "test"); + job(redundant,2,NULL,false,true,"test"); job(mixed,2,NULL,false,true,"test"); job(aliases,2,NULL,false,true,"test"); + const char *templates[] = {"trim", "trim-after-pages", "trim-after-job", "ptouch-chain"}; + for (int i=0;i<4;i++) job(NULL,0,templates[i],false,true,"test"); + job(NULL,0,NULL,true,true,"test"); job(NULL,0,NULL,false,true,"test"); + job(bad,1,NULL,false,false,"test"); job(NULL,0,"unknown",false,false,"test"); job(one+1,1,"trim",false,false,"test"); + update(NULL,0,"trim-after-job",false,true); defaults(63,"trim-after-job"); + job(NULL,0,NULL,false,true,"test"); update(one,1,NULL,false,true); defaults(3,NULL); + update(NULL,0,NULL,true,true); defaults(3,NULL); + update(NULL,0,"ptouch-chain",false,false); defaults(3,NULL); + update(one+2,1,"trim-after-job",false,false); defaults(3,NULL); + update(bad,1,NULL,false,false); defaults(3,NULL); + update(one+1,1,"trim-after-pages",false,true); defaults(11,"trim"); + update(redundant,2,NULL,false,true); defaults(60,"trim-after-pages"); + setter("3",true); defaults(3,NULL); roundtrip(temp,3,NULL); + setter("63",true); defaults(63,"trim-after-job"); roundtrip(temp,63,"trim-after-job"); + setter("60,63",true); defaults(60,"trim-after-pages"); + setter("4",false); defaults(60,"trim-after-pages"); + setter("ptouch-chain",false); defaults(60,"trim-after-pages"); + setter("3,999",false); defaults(60,"trim-after-pages"); + setter("60,",false); defaults(60,"trim-after-pages"); + setter(NULL,true); defaults(60,"trim-after-pages"); + // Attribute substitution never permits unsupported finishing choices. + job(bad,1,NULL,false,false,"test"); + copies("image/pwg-raster",2,false); copies("image/urf",2,false); + copies("image/pwg-raster",1,true); copies("image/png",999,true); copies(NULL,999,true); + // Ordinary legacy drivers retain fixed-mask acceptance and capabilities. + job(one+1,1,NULL,false,true,"legacy"); job(one+2,1,NULL,false,false,"legacy"); + job(NULL,0,"trim",false,true,"legacy"); job(NULL,0,"ptouch-chain",false,false,"legacy"); + httpClose(http); papplSystemShutdown(fixture_system); pthread_join(server,NULL); papplSystemDelete(fixture_system); + rmdir(temp); return 0; +} diff --git a/tests/test_pappl_raster.c b/tests/test_pappl_raster.c new file mode 100644 index 0000000..0ea1dbb --- /dev/null +++ b/tests/test_pappl_raster.c @@ -0,0 +1,294 @@ +/* Exercise pinned PAPPL through real IPP streaming and a TCP raster sink. + * This deliberately uses a minimal driver: the regression belongs at PAPPL's + * geometry/conversion boundary, independently of Brother finishing policy. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static unsigned job_starts, starts, rows, ends, clean_ends, abort_ends, end_calls; +static unsigned widths[4], heights[4], black_rows; +static int fail_row = -1, listener, port; +static bool fail_end; +static size_t sink_bytes; +static pappl_printer_t *printer; +static bool job_start(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) +{ (void)j; (void)o; (void)d; job_starts++; return true; } +static bool page_start(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d, unsigned p) +{ + (void)j; (void)d; (void)p; assert(starts < 4); + widths[starts] = o->header.cupsWidth; heights[starts++] = o->header.cupsHeight; + assert(o->header.cupsBitsPerPixel == 1); + assert(o->header.cupsBytesPerLine == (o->header.cupsWidth + 7) / 8); + return true; +} +static bool line_write(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d, + unsigned y, const unsigned char *line) +{ + (void)j; + if ((int)y == fail_row) return false; + /* The raster fixtures place one black pixel at each longitudinal edge. */ + assert(line[0] == 0 || line[0] == 0x80); + if (line[0]) assert(y == 0 || y == o->header.cupsHeight - 1); + for (unsigned x = 1; x < o->header.cupsBytesPerLine; x++) assert(line[x] == 0); + rows++; if (line[0] & 0x80) black_rows++; + return papplDeviceWrite(d, line, o->header.cupsBytesPerLine) == o->header.cupsBytesPerLine; +} +static bool page_end(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d, unsigned p) +{ (void)j; (void)o; (void)d; (void)p; ends++; return true; } +static bool job_end(pappl_job_t *j, pappl_pr_options_t *o, pappl_device_t *d) +{ + (void)o; (void)d; end_calls++; + if (papplJobIsCanceled(j) || papplJobGetState(j) == IPP_JSTATE_ABORTED) abort_ends++; + else clean_ends++; + return !fail_end; +} +static bool status(pappl_printer_t *p) { (void)p; return true; } +static bool driver(pappl_system_t *s, const char *n, const char *u, const char *id, + pappl_pr_driver_data_t *d, ipp_t **a, void *v) +{ + (void)s; (void)n; (void)u; (void)id; (void)a; (void)v; + strcpy(d->make_and_model, "Raster boundary fixture"); d->kind = PAPPL_KIND_LABEL; d->ppm = 20; + d->rstartjob_cb = job_start; d->rstartpage_cb = page_start; d->rwriteline_cb = line_write; + d->rendpage_cb = page_end; d->rendjob_cb = job_end; d->status_cb = status; + d->color_supported = PAPPL_COLOR_MODE_BI_LEVEL | PAPPL_COLOR_MODE_MONOCHROME; + d->color_default = PAPPL_COLOR_MODE_BI_LEVEL; + d->raster_types = PAPPL_PWG_RASTER_TYPE_BLACK_1 | PAPPL_PWG_RASTER_TYPE_BLACK_8; + d->force_raster_type = PAPPL_PWG_RASTER_TYPE_BLACK_1; + d->num_resolution = 1; d->x_resolution[0] = d->y_resolution[0] = 180; + d->x_default = d->y_default = 180; + d->num_source = 1; d->source[0] = "main-roll"; + d->num_type = 1; d->type[0] = "labels"; + d->num_media = 4; d->media[0] = "custom_12x100mm_12x100mm"; + d->media[1] = "roll_min_12x5mm"; d->media[2] = "roll_max_12x300mm"; + d->media[3] = "custom_12x25.4mm_12x25.4mm"; + strcpy(d->media_default.size_name, d->media[0]); + strcpy(d->media_default.source, "main-roll"); + strcpy(d->media_default.type, "labels"); + d->media_default.tracking = PAPPL_MEDIA_TRACKING_CONTINUOUS; + d->media_default.size_width = 1200; d->media_default.size_length = 10000; + d->media_ready[0] = d->media_default; + d->scaling_default = PAPPL_SCALING_NONE; + return true; +} +static void *run(void *s) { papplSystemRun(s); return NULL; } +static void *sink(void *v) +{ + (void)v; int peer = accept(listener, NULL, NULL); assert(peer >= 0); + char data[4096]; ssize_t n; + while ((n = read(peer, data, sizeof data)) > 0) sink_bytes += (size_t)n; + assert(n == 0); close(peer); return NULL; +} +static void fixture(const char *path, int bits, cups_mode_t mode, int pages, bool truncated) +{ + int fd = open(path, O_CREAT | O_TRUNC | O_WRONLY, 0600); assert(fd >= 0); + cups_raster_t *r = cupsRasterOpen(fd, mode); assert(r); + for (int p = 0; p < pages; p++) { + cups_page_header2_t h; + pwg_media_t media = {"custom_12x25.4mm", NULL, NULL, 1200, 2540 + p * 2540}; + assert(cupsRasterInitPWGHeader(&h, &media, bits == 1 ? "black_1" : bits == 8 ? "black_8" : "srgb_8", 180, 180, "one-sided", "normal")); + if (mode == CUPS_RASTER_WRITE_APPLE) h.cupsColorSpace = CUPS_CSPACE_SW; + h.cupsInteger[CUPS_RASTER_PWG_TotalPageCount] = 0; /* Optional count omitted. */ + assert(cupsRasterWriteHeader2(r, &h)); + unsigned char line[256]; assert(h.cupsBytesPerLine <= sizeof line); + for (unsigned y = 0; y < h.cupsHeight; y++) { + memset(line, mode == CUPS_RASTER_WRITE_APPLE ? 255 : 0, h.cupsBytesPerLine); + if (y == 0 || y == h.cupsHeight - 1) + line[0] = mode == CUPS_RASTER_WRITE_APPLE ? 0 : bits == 1 ? 128 : 255; + assert(cupsRasterWritePixels(r, line, h.cupsBytesPerLine) == h.cupsBytesPerLine); + } + } + cupsRasterClose(r); close(fd); + if (truncated) { + fd = open(path, O_RDWR); assert(fd >= 0); + off_t size = lseek(fd, 0, SEEK_END); assert(size > 8); + assert(!ftruncate(fd, size - 3)); close(fd); + } +} +static ssize_t failing_raster_read(void *ctx, unsigned char *buffer, size_t bytes) +{ + size_t n = fread(buffer, 1, bytes, ctx); + if (n) return (ssize_t)n; + errno = EIO; + return -1; +} +static void decoder_io_failure(const char *path, cups_mode_t mode) +{ + fixture(path, 8, mode, 1, false); + FILE *file = fopen(path, "rb"); assert(file); + cups_raster_t *r = cupsRasterOpenIO(failing_raster_read, file, CUPS_RASTER_READ); assert(r); + cups_page_header2_t header; assert(cupsRasterReadHeader2(r, &header)); + unsigned char line[256]; assert(header.cupsBytesPerLine <= sizeof line); + for (unsigned y = 0; y < header.cupsHeight; y++) + assert(cupsRasterReadPixels(r, line, header.cupsBytesPerLine) == header.cupsBytesPerLine); + assert(!cupsRasterReadHeader2(r, &header)); + const char *error = cupsRasterErrorString(); + assert(error && strstr(error, "Unable to read raster stream")); + cupsRasterClose(r); fclose(file); +} +static void execute(http_t *h, const char *path, const char *format, int explicit_size, + bool success, unsigned expected_rows, unsigned expected_pages) +{ + job_starts = starts = rows = ends = clean_ends = abort_ends = end_calls = black_rows = 0; sink_bytes = 0; + ipp_t *q = ippNewRequest(IPP_OP_PRINT_JOB); char uri[256]; + snprintf(uri, sizeof uri, "ipp://localhost:%d/ipp/print/test", port); + ippAddString(q, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri); + ippAddString(q, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, "raster-test"); + ippAddString(q, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, format); + if (explicit_size == 1) ippAddString(q, IPP_TAG_JOB, IPP_TAG_KEYWORD, "media", NULL, "custom_12x100mm_12x100mm"); + if (explicit_size > 1) { + ipp_t *media = ippNew(), *size = ippNew(); + if (explicit_size >= 4) + ippAddString(media, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "media-size-name", NULL, + explicit_size == 5 ? "custom_12x25.4mm_12x25.4mm" : "custom_12x100mm_12x100mm"); + if (explicit_size < 4 || explicit_size == 6) { + ippAddInteger(size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "x-dimension", 1200); + ippAddInteger(size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "y-dimension", explicit_size == 2 ? 10000 : 2540); + ippAddCollection(media, IPP_TAG_ZERO, "media-size", size); + } + ippAddCollection(q, IPP_TAG_JOB, "media-col", media); + ippDelete(size); ippDelete(media); + } + pthread_t reader; assert(!pthread_create(&reader, NULL, sink, NULL)); + ipp_t *response = cupsDoFileRequest(h, q, "/ipp/print/test", path); assert(response); + /* Streaming processing finishes before Print-Job returns. */ + pthread_join(reader, NULL); + int id = ippGetInteger(ippFindAttribute(response, "job-id", IPP_TAG_INTEGER), 0); + pappl_job_t *job = papplPrinterFindJob(printer, id); assert(job); + for (int i = 0; i < 200 && papplJobGetState(job) < IPP_JSTATE_CANCELED; i++) { + struct timespec delay = {0, 10000000L}; nanosleep(&delay, NULL); + } + fprintf(stderr, "%s: state=%d rows=%u pages=%u ends=%u/%u\n", path, papplJobGetState(job), rows, ends, clean_ends, abort_ends); + assert(papplJobGetState(job) == (success ? IPP_JSTATE_COMPLETED : IPP_JSTATE_ABORTED)); + assert(rows == expected_rows && ends == expected_pages && end_calls == job_starts); + if (explicit_size && !success && !expected_rows) assert(starts == 0); + assert(clean_ends == (unsigned)(success || fail_end) && abort_ends == (unsigned)(!success && !fail_end && job_starts)); + if (success && strcmp(format, "image/png")) { + assert(widths[0] == 85 && heights[0] == 180); + assert(black_rows == 2 * expected_pages); + if (expected_pages == 2) assert(widths[1] == 85 && heights[1] == 360); + } + assert(sink_bytes == rows * 11); + ippDelete(response); +} +int main(void) +{ + char temp[] = "/tmp/pappl-raster-XXXXXX"; assert(mkdtemp(temp)); + char path[256]; snprintf(path, sizeof path, "%s/page.ras", temp); + port = 22000 + getpid() % 15000; + pappl_system_t *s = papplSystemCreate(PAPPL_SOPTIONS_MULTI_QUEUE, "raster-test", port, + NULL, temp, "-", PAPPL_LOGLEVEL_ERROR, NULL, false); assert(s); + pappl_pr_driver_t d = {"fixture", "Fixture", NULL, NULL}; + papplSystemSetPrinterDrivers(s, 1, &d, NULL, NULL, driver, NULL); + listener = socket(AF_INET, SOCK_STREAM, 0); assert(listener >= 0); + struct sockaddr_in address = {.sin_family = AF_INET, .sin_addr.s_addr = htonl(INADDR_LOOPBACK)}; + assert(!bind(listener, (void *)&address, sizeof address)); + socklen_t len = sizeof address; assert(!getsockname(listener, (void *)&address, &len)); assert(!listen(listener, 1)); + char device[256]; snprintf(device, sizeof device, "socket://127.0.0.1:%d", ntohs(address.sin_port)); + printer = papplPrinterCreate(s, 0, "test", "fixture", "MFG:Test;MDL:Fixture;", device); assert(printer); + assert(papplSystemAddListeners(s, "localhost")); + pthread_t server; assert(!pthread_create(&server, NULL, run, s)); + http_t *h = httpConnect2("localhost", port, NULL, AF_UNSPEC, HTTP_ENCRYPTION_NEVER, 1, 3000, NULL); assert(h); + for (int bits = 1; bits <= 8; bits += 7) { + fixture(path, bits, CUPS_RASTER_WRITE_PWG, 1, false); + execute(h, path, "image/pwg-raster", false, true, 180, 1); + fixture(path, bits, CUPS_RASTER_WRITE_PWG, 2, false); + execute(h, path, "image/pwg-raster", false, true, 540, 2); + fixture(path, bits, CUPS_RASTER_WRITE_PWG, 1, true); + execute(h, path, "image/pwg-raster", false, false, 179, 0); + fixture(path, bits, CUPS_RASTER_WRITE_PWG, 1, false); + execute(h, path, "image/pwg-raster", true, false, 0, 0); + execute(h, path, "image/pwg-raster", 2, false, 0, 0); + execute(h, path, "image/pwg-raster", 3, true, 180, 1); + /* A concrete size-name is explicit geometry, and takes precedence + * over nested dimensions when both representations are present. */ + execute(h, path, "image/pwg-raster", 4, false, 0, 0); + execute(h, path, "image/pwg-raster", 5, true, 180, 1); + execute(h, path, "image/pwg-raster", 6, false, 0, 0); + fail_row = 3; + execute(h, path, "image/pwg-raster", false, false, 3, 0); + fail_row = -1; + } + for (int bits = 8; bits <= 8; bits += 7) { + fixture(path, bits, CUPS_RASTER_WRITE_APPLE, 1, false); + execute(h, path, "image/urf", false, true, 180, 1); + execute(h, path, "image/urf", 4, false, 0, 0); + execute(h, path, "image/urf", 5, true, 180, 1); + execute(h, path, "image/urf", 6, false, 0, 0); + } + /* A monochrome driver never receives unconverted RGB raster. */ + fixture(path, 24, CUPS_RASTER_WRITE_PWG, 1, false); + execute(h, path, "image/pwg-raster", false, false, 0, 0); + /* Invalid resolution is rejected before start-page or raster output. */ + fixture(path, 8, CUPS_RASTER_WRITE_PWG, 1, false); + int bad = open(path, O_WRONLY); assert(bad >= 0); + unsigned zero = 0; + assert(pwrite(bad, &zero, sizeof zero, 4 + offsetof(cups_page_header2_t, HWResolution)) == sizeof zero); + close(bad); + execute(h, path, "image/pwg-raster", false, false, 0, 0); + /* Unsupported resolution must not reinterpret 75dpi pixels at 180dpi. */ + fixture(path, 8, CUPS_RASTER_WRITE_PWG, 1, false); + bad = open(path, O_WRONLY); assert(bad >= 0); unsigned resolution[2] = {htonl(75), htonl(75)}; + assert(pwrite(bad, resolution, sizeof resolution, 4 + offsetof(cups_page_header2_t, HWResolution)) == sizeof resolution); + close(bad); execute(h, path, "image/pwg-raster", false, false, 0, 0); + /* Image cleanup must receive aborted state and must not repeat end-job. */ + FILE *image = fopen(path, "wb"); assert(image); + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + png_infop info = png_create_info_struct(png); assert(png && info); assert(!setjmp(png_jmpbuf(png))); + png_init_io(png, image); png_set_IHDR(png, info, 85, 180, 8, PNG_COLOR_TYPE_GRAY, + PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + png_write_info(png, info); unsigned char pixel[85]; memset(pixel, 255, sizeof pixel); + for (int y = 0; y < 180; y++) png_write_row(png, pixel); + png_write_end(png, info); png_destroy_write_struct(&png, &info); fclose(image); + fail_row = 3; execute(h, path, "image/png", false, false, 3, 0); fail_row = -1; + fail_end = true; execute(h, path, "image/png", false, false, 708, 1); fail_end = false; + /* Every malformed-termination case is part of the required CTest suite. */ + const cups_mode_t modes[] = {CUPS_RASTER_WRITE_PWG, CUPS_RASTER_WRITE_PWG, CUPS_RASTER_WRITE_APPLE}; + const int depths[] = {1, 8, 8}; + for (unsigned i = 0; i < 3; i++) { + const char *format = modes[i] == CUPS_RASTER_WRITE_APPLE ? "image/urf" : "image/pwg-raster"; + /* The next header can already be buffered by a compressed row read. */ + const unsigned fragments[] = {1, 17}; + for (unsigned f = 0; f < 2; f++) { + fixture(path, depths[i], modes[i], 1, false); + int fd = open(path, O_WRONLY | O_APPEND); assert(fd >= 0); + unsigned char partial[17] = {0}; + assert(write(fd, partial, fragments[f]) == fragments[f]); close(fd); + execute(h, path, format, false, false, 180, 1); + } + /* A complete but invalid next header must also fail, not become EOF. */ + fixture(path, depths[i], modes[i], 1, false); + int fd = open(path, O_WRONLY | O_APPEND); assert(fd >= 0); + unsigned char invalid[sizeof(cups_page_header2_t)] = {0}; + unsigned bytes = modes[i] == CUPS_RASTER_WRITE_APPLE ? 32 : sizeof invalid; + assert(write(fd, invalid, bytes) == bytes); close(fd); + execute(h, path, format, false, false, 180, 1); + /* A truncated first header emits no page setup or pixels. */ + fixture(path, depths[i], modes[i], 1, false); + fd = open(path, O_WRONLY); assert(fd >= 0); + assert(!ftruncate(fd, (modes[i] == CUPS_RASTER_WRITE_APPLE ? 12 : 4) + 1)); close(fd); + execute(h, path, format, false, false, 0, 0); + /* Failure in the second page cannot replay the first completed page. */ + fixture(path, depths[i], modes[i], 2, true); + execute(h, path, format, false, false, 539, 1); + fixture(path, depths[i], modes[i], 2, false); + execute(h, path, format, false, true, 540, 2); + } + httpClose(h); close(listener); papplSystemShutdown(s); + /* Let existing detached clients drain before deleting the fixture system. */ + pthread_join(server, NULL); papplSystemDelete(s); + /* A failed input callback is not normal EOF, with or without read-ahead. */ + decoder_io_failure(path, CUPS_RASTER_WRITE_PWG); + decoder_io_failure(path, CUPS_RASTER_WRITE_APPLE); + decoder_io_failure(path, CUPS_RASTER_WRITE); + unlink(path); rmdir(temp); return 0; +} diff --git a/tests/test_png_geometry.c b/tests/test_png_geometry.c new file mode 100644 index 0000000..cae6cdb --- /dev/null +++ b/tests/test_png_geometry.c @@ -0,0 +1,328 @@ +/* Real IPP image jobs and the production raster callbacks, captured without USB. */ +/* Tests must execute assertions in optimized builds too. */ +#ifdef NDEBUG +#undef NDEBUG +#endif +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "driver.h" +#include "filter_png.h" +#include "tables.h" + +static unsigned char wire[512000]; +static size_t used; +static unsigned fail_row; +static unsigned written_rows; +static bool truncate_png; +static pappl_job_t *last_job; +bool pt_usb_present(uint16_t vid, uint16_t pid) { (void)vid; (void)pid; return false; } +int pt_usb_count(uint16_t vid, uint16_t pid) { (void)vid; (void)pid; return 0; } +bool pt_usb_is_uri(const char *uri) { (void)uri; return false; } +ssize_t pt_usb_read_status(pappl_device_t *d, const char *uri, void *buffer, size_t bytes) +{ (void)d; (void)uri; (void)buffer; (void)bytes; assert(!"Unexpected USB read"); return -1; } +static bool device_open(pappl_device_t *d, const char *uri, const char *name) +{ (void)d; (void)uri; (void)name; return true; } +static void device_close(pappl_device_t *d) { (void)d; } +static ssize_t device_write(pappl_device_t *d, const void *buffer, size_t n) +{ + (void)d; + const unsigned char *p = buffer; + if (n && p[0] == 'G' && ++written_rows == fail_row) return -1; + assert(used + n <= sizeof wire); memcpy(wire + used, buffer, n); used += n; return n; +} +static pappl_preason_t device_status(pappl_device_t *d) { (void)d; return PAPPL_PREASON_NONE; } +static char *device_id(pappl_device_t *d, char *b, size_t n) +{ (void)d; snprintf(b, n, "MFG:Brother;MDL:PT-2730;"); return b; } +static bool printer_status(pappl_printer_t *p) { (void)p; return true; } +static bool driver(pappl_system_t *s, const char *name, const char *uri, const char *id, + pappl_pr_driver_data_t *data, ipp_t **attrs, void *cb) +{ + bool ok = pt_driver_cb(s, name, uri, id, data, attrs, cb); + data->status_cb = printer_status; return ok; +} +static void *server(void *s) { papplSystemRun(s); return NULL; } +static http_t *http; +static int port; +static const char resource[] = "/ipp/print/png"; +static void operation(ipp_t *request) +{ + char uri[128]; snprintf(uri, sizeof uri, "ipp://localhost:%d%s", port, resource); + ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_URI, "printer-uri", NULL, uri); + ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_NAME, "requesting-user-name", NULL, "test"); +} +static unsigned rows(unsigned char image[][128], unsigned *finishes) +{ + unsigned y = 0; *finishes = 0; + for (size_t pos = 0; pos < used;) { + if (wire[pos] == 'G') { + assert(wire[pos + 1] == 16 && y < 7000); + if (image) for (unsigned x = 0; x < 128; x++) + image[y][x] = !!(wire[pos + 3 + 15 - x / 8] & (1u << (x % 8))); + y++; pos += 19; + } else if (wire[pos] == 12 || wire[pos] == 26) { + ++*finishes; pos++; + } else { + assert(wire[pos] == 27); + if (wire[pos + 1] == '@') pos += 2; + else { assert(wire[pos + 1] == 'i'); pos += wire[pos + 2] == 'c' ? 8 : wire[pos + 2] == 'S' ? 3 : wire[pos + 2] == 'd' ? 5 : 4; } + } + } + return y; +} +/* Generated all-white 76x180 grayscale JPEG with 180 dpi metadata. */ +static const unsigned char jpeg_fixture[] = { + 0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10, 0x4a, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x01, 0x00, 0xb4, + 0x00, 0xb4, 0x00, 0x00, 0xff, 0xdb, 0x00, 0x43, 0x00, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0xff, 0xc0, 0x00, 0x0b, 0x08, 0x00, 0xb4, + 0x00, 0x4c, 0x01, 0x01, 0x11, 0x00, 0xff, 0xc4, 0x00, 0x1f, 0x00, 0x00, 0x01, 0x05, 0x01, 0x01, + 0x01, 0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x02, 0x03, 0x04, + 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5, 0x10, 0x00, 0x02, 0x01, 0x03, + 0x03, 0x02, 0x04, 0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01, 0x7d, 0x01, 0x02, 0x03, 0x00, + 0x04, 0x11, 0x05, 0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, 0x22, 0x71, 0x14, 0x32, + 0x81, 0x91, 0xa1, 0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, 0x24, 0x33, 0x62, 0x72, + 0x82, 0x09, 0x0a, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x34, 0x35, + 0x36, 0x37, 0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, + 0x56, 0x57, 0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, + 0x76, 0x77, 0x78, 0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, + 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, + 0xb3, 0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, + 0xca, 0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, + 0xe7, 0xe8, 0xe9, 0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, 0xf9, 0xfa, 0xff, 0xda, + 0x00, 0x08, 0x01, 0x01, 0x00, 0x00, 0x3f, 0x00, 0xfe, 0xfe, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, + 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, + 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, + 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, + 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, + 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, + 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, + 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, + 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, + 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, + 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, + 0xa2, 0x8a, 0x28, 0xa2, 0x8a, 0x28, 0xaf, 0xff, 0xd9, +}; +static void write_png(const char *path, int width, int height, int xppi, int yppi, + const unsigned char *pixels) +{ + FILE *file = fopen(path, "wb"); assert(file); + png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL); + png_infop info = png_create_info_struct(png); assert(png && info && !setjmp(png_jmpbuf(png))); + png_init_io(png, file); + png_set_IHDR(png, info, width, height, 8, PNG_COLOR_TYPE_GRAY, PNG_INTERLACE_NONE, + PNG_COMPRESSION_TYPE_DEFAULT, PNG_FILTER_TYPE_DEFAULT); + if (xppi) png_set_pHYs(png, info, (unsigned)(xppi / .0254 + .5), + (unsigned)(yppi / .0254 + .5), PNG_RESOLUTION_METER); + png_write_info(png, info); + for (int y = 0; y < height; y++) png_write_row(png, (png_bytep)(pixels + (size_t)y * width)); + png_write_end(png, info); png_destroy_write_struct(&png, &info); fclose(file); +} +/* media 0 omitted, 1 named50mm, 2 dimensions50mm, 3 source-only, 4 margin-only, + * 5 explicit10mm, 6 wrong tape18mm, 7 collection named50mm, + * 8 collection named25mm (too short), 9 collection named18mm (wrong tape), + * 10/11 custom12x40mm quantized by -/+1 centimm with explicit297 margins. */ +static void run_image(pappl_printer_t *printer, int width, int height, int xppi, int yppi, + const unsigned char *pixels, int orientation, int media, int copies, + const char *scaling, bool success, unsigned expected_rows) +{ + char path[] = "/tmp/ptouch-png-geometry-XXXXXX"; + int fd = mkstemp(path); assert(fd >= 0); close(fd); + if (pixels) write_png(path, width, height, xppi, yppi, pixels); + else { FILE *f = fopen(path, "wb"); assert(f); assert(fwrite(jpeg_fixture, 1, sizeof jpeg_fixture, f) == sizeof jpeg_fixture); fclose(f); } + if (truncate_png) { + FILE *f = fopen(path, "rb"); assert(f); assert(!fseek(f, 0, SEEK_END)); + long size = ftell(f); fclose(f); assert(size > 12 && !truncate(path, size - 12)); + } + ipp_t *request = ippNewRequest(IPP_OP_CREATE_JOB); operation(request); + ippAddString(request, IPP_TAG_JOB, IPP_TAG_KEYWORD, "job-hold-until", NULL, "indefinite"); + ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_INTEGER, "copies", copies); + ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_ENUM, "finishings", IPP_FINISHINGS_NONE); + ippAddString(request, IPP_TAG_JOB, IPP_TAG_KEYWORD, "print-scaling", NULL, scaling); + if (orientation) ippAddInteger(request, IPP_TAG_JOB, IPP_TAG_ENUM, "orientation-requested", orientation); + if (media == 1) ippAddString(request, IPP_TAG_JOB, IPP_TAG_KEYWORD, "media", NULL, "om_12x50mm_12x50mm"); + else if (media) { + ipp_t *col = ippNew(); + if (media >= 7 && media <= 9) ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "media-size-name", NULL, + media == 8 ? "om_12x25mm_12x25mm" : + media == 9 ? "om_18x50mm_18x50mm" : "om_12x50mm_12x50mm"); + else if (media == 3) ippAddString(col, IPP_TAG_ZERO, IPP_TAG_KEYWORD, "media-source", NULL, "main-roll"); + else if (media == 4) ippAddInteger(col, IPP_TAG_ZERO, IPP_TAG_INTEGER, "media-top-margin", 0); + else { + ipp_t *size = ippNew(); + ippAddInteger(size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "x-dimension", media == 6 ? 1800 : media == 10 ? 1199 : media == 11 ? 1201 : 1200); + ippAddInteger(size, IPP_TAG_ZERO, IPP_TAG_INTEGER, "y-dimension", media == 5 ? 1000 : media >= 10 ? 3986 : 5000); + ippAddCollection(col, IPP_TAG_ZERO, "media-size", size); ippDelete(size); + if (media >= 10) { + ippAddInteger(col, IPP_TAG_ZERO, IPP_TAG_INTEGER, "media-left-margin", 297); + ippAddInteger(col, IPP_TAG_ZERO, IPP_TAG_INTEGER, "media-right-margin", 297); + } + } + ippAddCollection(request, IPP_TAG_JOB, "media-col", col); ippDelete(col); + } + ipp_t *response = cupsDoRequest(http, request, resource); + if (!response || ippGetStatusCode(response) > IPP_STATUS_OK_EVENTS_COMPLETE) + fprintf(stderr, "create geometry job: %s\n", cupsLastErrorString()); + assert(response && ippGetStatusCode(response) <= IPP_STATUS_OK_EVENTS_COMPLETE); + int id = ippGetInteger(ippFindAttribute(response, "job-id", IPP_TAG_INTEGER), 0); ippDelete(response); + pappl_job_t *j = papplPrinterFindJob(printer, id); assert(j); last_job = j; + request = ippNewRequest(IPP_OP_SEND_DOCUMENT); operation(request); + ippAddInteger(request, IPP_TAG_OPERATION, IPP_TAG_INTEGER, "job-id", id); + ippAddString(request, IPP_TAG_OPERATION, IPP_TAG_MIMETYPE, "document-format", NULL, pixels ? "image/png" : "image/jpeg"); + ippAddBoolean(request, IPP_TAG_OPERATION, "last-document", true); + response = cupsDoFileRequest(http, request, resource, path); unlink(path); + assert(response && ippGetStatusCode(response) <= IPP_STATUS_OK_EVENTS_COMPLETE); ippDelete(response); + used = written_rows = 0; assert(papplJobRelease(j, "test")); + for (int i = 0; i < 1000 && papplJobGetState(j) < IPP_JSTATE_CANCELED; i++) { + struct timespec delay = {0, 10000000}; nanosleep(&delay, NULL); + } + if (papplJobGetState(j) != (success ? IPP_JSTATE_COMPLETED : IPP_JSTATE_ABORTED)) + fprintf(stderr, "PNG %dx%d media%d orientation%d: state%d message%s\n", width, height, media, orientation, papplJobGetState(j), papplJobGetMessage(j)); + assert(papplJobGetState(j) == (success ? IPP_JSTATE_COMPLETED : IPP_JSTATE_ABORTED)); + unsigned finishes; + unsigned actual_rows = rows(NULL, &finishes); + if (actual_rows != expected_rows) + fprintf(stderr, "PNG media%d: got %u raster rows, expected %u\n", media, actual_rows, expected_rows); + assert(actual_rows == expected_rows); + if (!success) assert(!finishes); +} +int main(void) +{ + pappl_pr_driver_t drv = {"ptouch", "Test", NULL, NULL}; + papplDeviceAddScheme("pngfixture", PAPPL_DEVTYPE_CUSTOM_LOCAL, NULL, device_open, + device_close, NULL, device_write, device_status, device_id); + port = 30000 + getpid() % 15000; + pappl_system_t *sys = papplSystemCreate(PAPPL_SOPTIONS_MULTI_QUEUE, "png-geometry", port, + NULL, NULL, "/tmp/ptouch-png-geometry.log", PAPPL_LOGLEVEL_ERROR, NULL, false); assert(sys); + papplSystemSetPrinterDrivers(sys, 1, &drv, NULL, NULL, driver, NULL); + papplSystemAddMIMEFilter(sys, "image/png", "image/png", pt_filter_png, NULL); + pappl_printer_t *p = papplPrinterCreate(sys, 0, "png", "ptouch", "MFG:Brother;MDL:PT-2730;", "pngfixture://capture"); assert(p); + assert(papplSystemAddListeners(sys, "localhost")); + pthread_t thread; assert(!pthread_create(&thread, NULL, server, sys)); + http = httpConnect2("localhost", port, NULL, AF_UNSPEC, HTTP_ENCRYPTION_NEVER, 1, 3000, NULL); assert(http); + ipp_t *query = ippNewRequest(IPP_OP_GET_PRINTER_ATTRIBUTES); operation(query); + ipp_t *reply = cupsDoRequest(http, query, resource); assert(reply); + ipp_attribute_t *formats = ippFindAttribute(reply, "document-format-supported", IPP_TAG_MIMETYPE); + assert(formats); int png_count = 0; + for (int i = 0; i < ippGetCount(formats); i++) { + const char *format = ippGetString(formats, i, NULL); + png_count += !strcmp(format, "image/png"); + assert(strcmp(format, "image/vnd.ptouch-raster")); + } + assert(png_count == 1); ippDelete(reply); + run_image(p, 76, 180, 180, 180, NULL, 3, 1, 1, "none", true, 354); + run_image(p, 76, 180, 180, 180, NULL, 3, 2, 1, "none", true, 354); + run_image(p, 76, 180, 180, 180, NULL, 3, 0, 1, "none", true, 708); + unsigned char *pixels = malloc(3000 * 300); assert(pixels); + memset(pixels, 255, 3000 * 300); + pixels[5 * 76 + 2] = pixels[17 * 76 + 70] = 0; + for (int m = 0; m <= 4; m++) run_image(p, 76, 180, 180, 180, pixels, 3, m, 1, "none", true, m == 1 || m == 2 ? 354 : 180); + run_image(p, 76, 180, 180, 180, pixels, 3, 7, 1, "none", true, 354); + run_image(p, 76, 180, 180, 180, pixels, 3, 8, 1, "none", false, 0); + run_image(p, 76, 180, 180, 180, pixels, 3, 9, 1, "none", false, 0); + run_image(p, 42, 180, 180, 180, pixels, 3, 10, 1, "none", true, 282); + run_image(p, 42, 180, 180, 180, pixels, 3, 11, 1, "none", true, 282); + run_image(p, 76, 180, 180, 180, pixels, 3, 10, 1, "none", false, 0); + assert(strstr(papplJobGetMessage(last_job), "printable width is 42")); + run_image(p, 76, 180, 0, 0, pixels, 3, 0, 3, "none", true, 540); + run_image(p, 76, 180, 180, 180, pixels, 3, 5, 1, "none", false, 0); + run_image(p, 76, 180, 180, 180, pixels, 3, 6, 1, "none", false, 0); + run_image(p, 76, 180, 180, 360, pixels, 3, 0, 1, "none", false, 0); + memset(pixels, 255, 3000 * 300); pixels[0] = pixels[200 * 299 + 199] = 0; + run_image(p, 200, 300, 180, 180, pixels, 3, 0, 1, "none", false, 0); + memset(pixels, 0, 200 * 300); + const char *scales[] = {"fit", "auto-fit", "auto", "fill"}; + static unsigned char scaled[7000][128]; + for (int c = 0; c < 4; c++) { + run_image(p, 200, 300, 180, 180, pixels, 3, 2, 1, scales[c], true, 354); + unsigned finished; assert(rows(scaled, &finished) == 354); + for (unsigned y = 0; y < 354; y++) for (unsigned x = 0; x < 128; x++) { + bool black = x >= 26 && x < 102 && (c == 3 || (y >= 120 && y < 234)); + assert(scaled[y][x] == black); + } + } + /* Compare every head dot of every raster row for asymmetric orientation markers. */ + static unsigned char actual[7000][128]; + const int orientations[] = {3, 6, 4, 5, 0}; + for (int c = 0; c < 5; c++) { + int o = orientations[c], w = c >= 2 ? 180 : 76, h = c >= 2 ? 76 : 180; + memset(pixels, 255, w * h); + int mx[] = {2, w - 3, 11}, my[] = {5, h - 7, 19}; + for (int i = 0; i < 3; i++) pixels[my[i] * w + mx[i]] = 0; + run_image(p, w, h, 180, 180, pixels, o, 0, 1, "none", true, 180); + unsigned finishes; assert(rows(actual, &finishes) == 180); + unsigned char expected[180][128] = {{0}}; + for (int i = 0; i < 3; i++) { + int x, y; + if (o == 3) { x = mx[i]; y = my[i]; } + else if (o == 6) { x = w - 1 - mx[i]; y = h - 1 - my[i]; } + else if (o == 4) { x = my[i]; y = w - 1 - mx[i]; } + else { x = h - 1 - my[i]; y = mx[i]; } + expected[y][26 + x] = 1; + } + if (memcmp(actual, expected, sizeof expected)) + for (unsigned y = 0; y < 180; y++) for (unsigned x = 0; x < 128; x++) + if (actual[y][x] != expected[y][x]) fprintf(stderr, "orientation%d dot%u,%u actual%d expected%d\n", o, x, y, actual[y][x], expected[y][x]); + assert(!memcmp(actual, expected, sizeof expected)); + } + unsigned finishes; + pt_geometry g; assert(pt_tape_geometry(12, &g)); + for (int c = 0; c < 4; c++) { + int o = orientations[c], w = c < 2 ? g.nominal_px : 180, h = c < 2 ? 180 : g.nominal_px; + int first = o == 6 || o == 5 ? g.nominal_px - g.right_px : g.left_px; + memset(pixels, 255, w * h); + int mx[] = {c < 2 ? first : 5, c < 2 ? first + g.imageable_px - 1 : 171}; + int my[] = {c < 2 ? 5 : first, c < 2 ? 171 : first + g.imageable_px - 1}; + for (int i = 0; i < 2; i++) pixels[my[i] * w + mx[i]] = 0; + run_image(p, w, h, 180, 180, pixels, o, 0, 1, "none", true, 180); + rows(actual, &finishes); + unsigned char expected[180][128] = {{0}}; + for (int i = 0; i < 2; i++) { + int x, y; + if (o == 3) { x = mx[i] - first; y = my[i]; } + else if (o == 6) { x = g.imageable_px - 1 - (mx[i] - first); y = h - 1 - my[i]; } + else if (o == 4) { x = my[i] - first; y = w - 1 - mx[i]; } + else { x = g.imageable_px - 1 - (my[i] - first); y = mx[i]; } + expected[y][26 + x] = 1; + } + assert(!memcmp(actual, expected, sizeof expected)); + pixels[0] = 0; + run_image(p, w, h, 180, 180, pixels, o, 0, 1, "none", false, 0); + } + memset(pixels, 255, 152 * 360); + for (int dy = 0; dy < 2; dy++) for (int dx = 0; dx < 2; dx++) + pixels[(10 + dy) * 152 + 4 + dx] = 0; + run_image(p, 152, 360, 360, 360, pixels, 3, 0, 1, "none", true, 180); + rows(actual, &finishes); assert(actual[5][28]); + memset(pixels, 255, 3000 * 300); + run_image(p, 76, 1, 180, 180, pixels, 3, 0, 1, "none", true, 35); + run_image(p, 76, 2125, 180, 180, pixels, 3, 0, 1, "none", true, 2125); + run_image(p, 76, 2126, 180, 180, pixels, 3, 0, 1, "none", false, 0); + truncate_png = true; + run_image(p, 76, 180, 180, 180, pixels, 3, 0, 1, "none", false, 0); + truncate_png = false; + fail_row = 7; + run_image(p, 76, 180, 180, 180, pixels, 3, 0, 1, "none", false, 6); + assert(written_rows == 7); /* No later raster follows the failing callback. */ + size_t before_cleanup = used; + pappl_pr_driver_data_t dd; papplPrinterGetDriverData(p, &dd); + pappl_pr_options_t *options = papplJobCreatePrintOptions(last_job, 1, false); assert(options); + assert(dd.rendjob_cb(last_job, options, NULL)); + assert(dd.rendjob_cb(last_job, options, NULL)); + assert(used == before_cleanup && !papplJobGetData(last_job)); + papplJobDeletePrintOptions(options); + free(pixels); httpClose(http); papplSystemShutdown(sys); + /* Do not add a detached PAPPL client while the system is shutting down. */ + pthread_join(thread, NULL); papplSystemDelete(sys); + puts("PNG IPP geometry, copies, blank margins, scaling, and complete raster markers passed"); + return 0; +} diff --git a/tests/test_tables.c b/tests/test_tables.c index 39bf97d..c4c5262 100644 --- a/tests/test_tables.c +++ b/tests/test_tables.c @@ -19,5 +19,19 @@ int main(void) { assert(pt_tape_px(9) == 52); assert(pt_tape_px(6) == 32); assert(pt_tape_px(99) == 0); + const int widths[] = {6, 9, 12, 18, 24}; + const int nominal[] = {42, 63, 85, 127, 170}; + const int imageable[] = {32, 52, 76, 120, 128}; + for (unsigned i = 0; i < sizeof widths / sizeof widths[0]; i++) { + pt_geometry g; + assert(pt_tape_geometry(widths[i], &g)); + assert(g.nominal_px == nominal[i] && g.imageable_px == imageable[i]); + assert(g.left_px == (nominal[i] - imageable[i]) / 2); + assert(g.right_px - g.left_px == imageable[i] && g.right_px <= nominal[i]); + assert(g.margin_centimm >= 53); + } + pt_geometry g; + assert(pt_tape_geometry(18, &g) && g.margin_centimm == 53); + assert(!pt_tape_geometry(36, &g) && !pt_tape_geometry(99, &g)); return 0; }