[Bug/Critical] Print output mirrored + bottom-anchored target canvas (#211) #213

Merged
gronod merged 2 commits from bugfix/211-print-output-flip into develop 2026-09-17 21:59:47 +01:00
Owner

Summary

Fixes #211 — print output was mirrored since M12. TargetPageCanvasView.isFlipped = true put the canvas in a top-down CTM where CGContext.draw(_:in:) renders the CGImage upside-down.

PR #212 (now un-merged; merge commit 273c319 removed from develop) set isFlipped = false — correct for the mirror — but left destinationRect(forPage:) anchored at pageRect.minY, which is the page's bottom edge in bottom-up space. Sub-page targets would have printed at the foot of the sheet and oversize targets would clip their top rows. This PR re-lands the un-flip and restores the spec'd top-left anchor (docs/14 §6, D9): y = snap(pageRect.maxY - size.height).

Root Cause Analysis

  • CGContext.draw(_:in:) draws a CGImage upright in a bottom-up (y-up) CTM and upside-down in a flipped (y-down) CTM — the flipped view produced the mirrored pages.
  • After un-flipping, pageRect.minY changed meaning from top edge to bottom edge; the anchor expression was not updated.
  • Existing coverage could not catch either defect: all geometry fixtures are ~full-page rasters (the two anchors differ by ~0.03 pt), NativeSpoolPDFTests located the painted origin dynamically, and every fixture was a solid fill — a mirrored solid block is pixel-identical.

Fix

  • TargetPageCanvasView: isFlipped = false (bottom-up space); destinationRect anchors y = snap(pageRect.maxY - size.height); stale flipped-space comments corrected (rectForPage stacks bottom-up; page 1 is the bottom band, the non-flipped pagination convention).
  • TargetCanvasGeometryTests: testDestinationRectAnchorsTopLeftAndSnaps now asserts the top anchor with a sub-page 72×72 pt raster (y == paperHeight - 72) plus the near-full-page clip edge (y ≈ -0.03); testRectForPageStacksBottomUp renamed/documented.
  • TargetRasterFixtures: makeImage/makeTIFF accept bottomHalfPixelBytes — a top/bottom banded image for orientation assertions.
  • NativeSpoolPDFTests: new testSavePDFDrawsPageUprightTopAnchored saves a 144 pt red-over-blue block via .save PDF and asserts red band at the painted top edge, blue below it, white beneath the block — fails on both a mirrored draw and a bottom anchor. Shared makeSaveInfo/renderPageToBitmap helpers extracted.

Verification

xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS=x86_64 — TargetCanvasGeometryTests, NativeSpoolPDFTests, TargetRasterTests: 23/23 pass, including the new banded-orientation test.

Closes #211

## Summary Fixes #211 — print output was mirrored since M12. `TargetPageCanvasView.isFlipped = true` put the canvas in a top-down CTM where `CGContext.draw(_:in:)` renders the CGImage upside-down. PR #212 (now un-merged; merge commit 273c319 removed from `develop`) set `isFlipped = false` — correct for the mirror — but left `destinationRect(forPage:)` anchored at `pageRect.minY`, which is the page's **bottom** edge in bottom-up space. Sub-page targets would have printed at the foot of the sheet and oversize targets would clip their top rows. This PR re-lands the un-flip **and** restores the spec'd top-left anchor (docs/14 §6, D9): `y = snap(pageRect.maxY - size.height)`. ## Root Cause Analysis - `CGContext.draw(_:in:)` draws a CGImage upright in a bottom-up (y-up) CTM and upside-down in a flipped (y-down) CTM — the flipped view produced the mirrored pages. - After un-flipping, `pageRect.minY` changed meaning from top edge to bottom edge; the anchor expression was not updated. - Existing coverage could not catch either defect: all geometry fixtures are ~full-page rasters (the two anchors differ by ~0.03 pt), `NativeSpoolPDFTests` located the painted origin dynamically, and every fixture was a solid fill — a mirrored solid block is pixel-identical. ## Fix - `TargetPageCanvasView`: `isFlipped = false` (bottom-up space); `destinationRect` anchors `y = snap(pageRect.maxY - size.height)`; stale flipped-space comments corrected (`rectForPage` stacks bottom-up; page 1 is the bottom band, the non-flipped pagination convention). - `TargetCanvasGeometryTests`: `testDestinationRectAnchorsTopLeftAndSnaps` now asserts the top anchor with a sub-page 72×72 pt raster (`y == paperHeight - 72`) plus the near-full-page clip edge (`y ≈ -0.03`); `testRectForPageStacksBottomUp` renamed/documented. - `TargetRasterFixtures`: `makeImage`/`makeTIFF` accept `bottomHalfPixelBytes` — a top/bottom banded image for orientation assertions. - `NativeSpoolPDFTests`: new `testSavePDFDrawsPageUprightTopAnchored` saves a 144 pt red-over-blue block via `.save` PDF and asserts red band at the painted top edge, blue below it, white beneath the block — fails on both a mirrored draw and a bottom anchor. Shared `makeSaveInfo`/`renderPageToBitmap` helpers extracted. ## Verification `xcodebuild test -scheme ICCery -destination 'platform=macOS' ARCHS=x86_64` — TargetCanvasGeometryTests, NativeSpoolPDFTests, TargetRasterTests: 23/23 pass, including the new banded-orientation test. Closes #211
gronod added the Kind/Bug
Priority
Critical
1
Project/ICCery-v2Bug/Backend
labels 2026-09-17 21:54:12 +01:00
gronod added 2 commits 2026-09-17 21:54:14 +01:00
test(print): flip canvas coordinate space to bottom-up for NSPrintOperation compatibility
macOS CI / build-and-test (push) Skipped
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 34s
226e7b2a78
- TargetPageCanvasView.isFlipped: true → false (bottom-up CoreGraphics origin)
- TargetCanvasGeometryTests: assertion updated to XCTAssertFalse(view.isFlipped)
fix(print): anchor raster at page top in bottom-up canvas space (#211)
macOS CI / package (pull_request) Canceled after 0s
macOS CI / build-and-test (pull_request) Canceled after 11m49s
macOS CI / build-and-test (push) Failing after 44m23s
macOS CI / package (push) Skipped
ddb9245f44
226e7b2 un-flipped the canvas but left destinationRect anchored at
pageRect.minY — the page's BOTTOM edge in bottom-up space — so
sub-page targets printed at the foot of the sheet and oversize
targets clipped their top rows. Anchor at pageRect.maxY - height,
restoring the spec'd top-left anchor (docs/14 §6, D9).

Adds the missing regression coverage: the geometry test now asserts
the top anchor with a sub-page raster, and the PDF harness gains a
banded (red-over-blue) fixture that fails on either a mirrored draw
or a bottom anchor — solid fills could catch neither.
gronod merged commit ddb9245f44 into develop 2026-09-17 21:59:47 +01:00
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: gronod/iccery-v2-mac#213