@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +57 -0
- package/PdfCanvas.podspec +77 -40
- package/README.md +218 -1316
- package/android/build.gradle +67 -54
- package/android/consumer-rules.pro +7 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +254 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/TestPdfs.java +60 -468
- package/android/src/main/cpp/CMakeLists.txt +54 -0
- package/android/src/main/cpp/pdfcanvas-jni.cpp +262 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +167 -0
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfErrorCode.java +14 -4
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRasterException.java +10 -4
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +160 -376
- package/android/tools/compile-gate.sh +81 -101
- package/dist/controller.d.ts +14 -0
- package/dist/controller.js +4 -3
- package/dist/rasterizer/index.native.d.ts +8 -12
- package/dist/rasterizer/index.native.js +15 -21
- package/dist/rasterizer/native-bridge.d.ts +51 -75
- package/dist/rasterizer/native-bridge.js +49 -33
- package/dist/rasterizer/native.d.ts +45 -0
- package/dist/rasterizer/native.js +123 -0
- package/dist/react/usePdfDocument.d.ts +5 -5
- package/dist/react/usePdfLayer.d.ts +10 -1
- package/dist/react/usePdfLayer.js +10 -2
- package/dist/testing/scenes.d.ts +3 -3
- package/dist/testing/scenes.js +4 -4
- package/dist/types.d.ts +36 -42
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +234 -536
- package/native/.clangd +5 -0
- package/native/CMakeLists.txt +89 -0
- package/native/core/include/pdfcanvas/document.h +90 -0
- package/native/core/include/pdfcanvas/error.h +47 -0
- package/native/core/include/pdfcanvas/jsi.h +18 -0
- package/native/core/include/pdfcanvas/jsi_config.h +30 -0
- package/native/core/include/pdfcanvas/library.h +28 -0
- package/native/core/include/pdfcanvas/pixels.h +92 -0
- package/native/core/include/pdfcanvas/service.h +87 -0
- package/native/core/include/pdfcanvas/slots.h +113 -0
- package/native/core/include/pdfcanvas/types.h +110 -0
- package/native/core/pdfcanvas-core.cmake +22 -0
- package/native/core/src/document.cpp +516 -0
- package/native/core/src/error.cpp +29 -0
- package/native/core/src/jsi.cpp +66 -0
- package/native/core/src/library.cpp +52 -0
- package/native/core/src/pixels.cpp +45 -0
- package/native/core/src/service.cpp +156 -0
- package/native/core/src/slots.cpp +139 -0
- package/native/tests/fixtures.cpp +451 -0
- package/native/tests/fixtures.h +96 -0
- package/native/tests/harness.h +122 -0
- package/native/tests/main.cpp +64 -0
- package/native/tests/pixels_util.h +126 -0
- package/native/tests/test_document.cpp +485 -0
- package/native/tests/test_service.cpp +130 -0
- package/native/tests/test_slots.cpp +250 -0
- package/package.json +27 -7
- package/react-native.config.js +5 -2
- package/scripts/fetch-pdfium.mjs +559 -0
- package/scripts/pdfium-manifest.json +48 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasCancellationTest.java +0 -226
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasConcurrencyTest.java +0 -239
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasDeviceTimingTest.java +0 -251
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasFailureTest.java +0 -221
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasJsiTransportTest.java +0 -783
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPhaseTimingTest.java +0 -1388
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasPoisonGuardTest.java +0 -98
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasQuirksTest.java +0 -324
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasRasterizerTest.java +0 -775
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PoisonsTheProcess.java +0 -20
- package/android/src/jsi/cpp/CMakeLists.txt +0 -33
- package/android/src/jsi/cpp/pdfcanvas-jsi.cpp +0 -469
- package/android/src/jsi/java/tools/reekon/pdfcanvas/jsi/PdfCanvasPixelBridge.java +0 -140
- package/android/src/main/java/tools/reekon/pdfcanvas/PageGeometry.java +0 -45
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasRasterizer.java +0 -564
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPlatformQuirks.java +0 -277
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfPreflight.java +0 -214
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfRendererPool.java +0 -234
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterCancellation.java +0 -47
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterPixels.java +0 -143
- package/android/src/main/java/tools/reekon/pdfcanvas/RasterRequest.java +0 -95
- package/dist/rasterizer/android.d.ts +0 -121
- package/dist/rasterizer/android.js +0 -126
- package/dist/rasterizer/ios.d.ts +0 -84
- package/dist/rasterizer/ios.js +0 -155
- package/ios/Package.swift +0 -58
- package/ios/Sources/PdfCanvasCore/PdfCanvasCancellation.m +0 -38
- package/ios/Sources/PdfCanvasCore/PdfCanvasDocumentPool.m +0 -389
- package/ios/Sources/PdfCanvasCore/PdfCanvasErrorCode.m +0 -57
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageGeometry.m +0 -44
- package/ios/Sources/PdfCanvasCore/PdfCanvasPageTransform.m +0 -82
- package/ios/Sources/PdfCanvasCore/PdfCanvasPixelSink.m +0 -37
- package/ios/Sources/PdfCanvasCore/PdfCanvasPreflight.m +0 -158
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterPixels.m +0 -80
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterRequest.m +0 -65
- package/ios/Sources/PdfCanvasCore/PdfCanvasRasterizer.m +0 -548
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCancellation.h +0 -48
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasCore.h +0 -26
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasDocumentPool.h +0 -122
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasErrorCode.h +0 -80
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageGeometry.h +0 -79
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPageTransform.h +0 -129
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPixelSink.h +0 -100
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasPreflight.h +0 -65
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterPixels.h +0 -111
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterRequest.h +0 -85
- package/ios/Sources/PdfCanvasCore/include/PdfCanvasRasterizer.h +0 -163
- package/ios/Sources/PdfCanvasSlots/PdfCanvasSlots.mm +0 -297
- package/ios/Sources/PdfCanvasSlots/include/PdfCanvasSlots.h +0 -140
- package/ios/Tests/PdfCanvasCoreTests/ConcurrencyTests.swift +0 -358
- package/ios/Tests/PdfCanvasCoreTests/FailureTests.swift +0 -262
- package/ios/Tests/PdfCanvasCoreTests/GeometryTests.swift +0 -173
- package/ios/Tests/PdfCanvasCoreTests/PreviewTests.swift +0 -114
- package/ios/Tests/PdfCanvasCoreTests/RasterAssertions.swift +0 -158
- package/ios/Tests/PdfCanvasCoreTests/RasterizerTests.swift +0 -544
- package/ios/Tests/PdfCanvasCoreTests/SinkTests.swift +0 -287
- package/ios/Tests/PdfCanvasCoreTests/SlotsTests.swift +0 -396
- package/ios/Tests/PdfCanvasCoreTests/TestPdfs.swift +0 -639
- package/ios/Tests/PdfCanvasCoreTests/TimingTests.swift +0 -179
- package/ios/Tests/PdfCanvasCoreTests/TransformTests.swift +0 -283
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
import Foundation
|
|
2
|
-
import XCTest
|
|
3
|
-
|
|
4
|
-
@testable import PdfCanvasCore
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
Numbers, printed rather than asserted.
|
|
8
|
-
|
|
9
|
-
THESE ARE NOT PASS/FAIL TESTS AND MUST NOT BECOME ONES. Their counterpart is
|
|
10
|
-
`PdfCanvasDeviceTimingTest` on Android, whose own lesson is written into
|
|
11
|
-
`AndroidRasterizerOptions.poolSize`: a synthetic fixture measured two lanes at
|
|
12
|
-
1.06x-1.25x while a real drawing measured 0.82x, and the synthetic number would
|
|
13
|
-
have shipped the wrong default. So what these produce is a SHAPE, on whatever
|
|
14
|
-
machine ran them, and the numbers that decide anything come from a device with a
|
|
15
|
-
real document on it — `app/dev-pdf-canvas-probe.tsx` in the consuming app.
|
|
16
|
-
|
|
17
|
-
The one thing they do assert is that nothing failed, because a timing loop that
|
|
18
|
-
silently rendered zero tiles would still print a very fast number.
|
|
19
|
-
*/
|
|
20
|
-
final class TimingTests: XCTestCase {
|
|
21
|
-
|
|
22
|
-
private func measure(_ label: String, _ body: () -> Void) -> Double {
|
|
23
|
-
var best = Double.infinity
|
|
24
|
-
for _ in 0..<5 {
|
|
25
|
-
let started = Date()
|
|
26
|
-
body()
|
|
27
|
-
best = min(best, Date().timeIntervalSince(started) * 1000)
|
|
28
|
-
}
|
|
29
|
-
print("PDFCANVAS-TIMING \(label): \(String(format: "%.2f", best)) ms")
|
|
30
|
-
return best
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
Per-tile cost at the sizes the planner actually asks for.
|
|
35
|
-
|
|
36
|
-
`maxPieceDimension` is 1024, so 512² and 1024² are the two that matter, and
|
|
37
|
-
`minEpochIntervalMs` is defined as the measured p95 of one of them.
|
|
38
|
-
*/
|
|
39
|
-
func testTileCostBySize() throws {
|
|
40
|
-
let r = try openFixture(
|
|
41
|
-
TestPdfs.grid(width: 1728, height: 2592, pitch: 8), name: "timing-tiles.pdf")
|
|
42
|
-
var rendered = 0
|
|
43
|
-
for side in [256, 512, 1024] {
|
|
44
|
-
let scale = 4.0
|
|
45
|
-
_ = measure("tile \(side)px @4x") {
|
|
46
|
-
if (try? r.render(
|
|
47
|
-
request(
|
|
48
|
-
x: 100, y: 100, width: Double(side) / scale, height: Double(side) / scale,
|
|
49
|
-
scale: scale),
|
|
50
|
-
cancellation: nil, sink: nil)) != nil
|
|
51
|
-
{
|
|
52
|
-
rendered += 1
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
XCTAssertEqual(rendered, 15, "every measured render must have succeeded")
|
|
57
|
-
r.close()
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
A whole ARCH-D base raster at a plausible fit scale.
|
|
62
|
-
|
|
63
|
-
The base layer is `fitZoom * pixelRatio * baseQuality` over the whole sheet, so
|
|
64
|
-
on a DPR-3 phone an ARCH-D page is roughly 1170x1755 — which is what this
|
|
65
|
-
approximates. It is the single largest raster the package ever asks a backend
|
|
66
|
-
for, and therefore the one that sets the worst-case first paint.
|
|
67
|
-
*/
|
|
68
|
-
func testWholePageBaseRasterCost() throws {
|
|
69
|
-
let r = try openFixture(
|
|
70
|
-
TestPdfs.grid(width: 1728, height: 2592, pitch: 8), name: "timing-base.pdf")
|
|
71
|
-
var rendered = 0
|
|
72
|
-
_ = measure("ARCH-D base @0.677x (1170x1755)") {
|
|
73
|
-
if (try? r.render(
|
|
74
|
-
request(width: 1728, height: 2592, scale: 0.677), cancellation: nil,
|
|
75
|
-
sink: nil)) != nil
|
|
76
|
-
{
|
|
77
|
-
rendered += 1
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
XCTAssertEqual(rendered, 5)
|
|
81
|
-
r.close()
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/**
|
|
85
|
-
Lane scaling, over the schedule the controller actually runs: N lanes pulling
|
|
86
|
-
from one shared cursor of plan items.
|
|
87
|
-
|
|
88
|
-
ON THIS MACHINE CoreGraphics parallelises — the probe measured 1.53x at two lanes
|
|
89
|
-
and 2.28x at four on a dense ARCH-D page, which is the OPPOSITE of Android, where
|
|
90
|
-
AOSP serialises `PdfRenderer` behind a process-wide PDFium lock that convoys and
|
|
91
|
-
every lane count above one measures worse. That difference is why
|
|
92
|
-
`IosRasterizerOptions.poolSize` does not cite any Android evidence.
|
|
93
|
-
*
|
|
94
|
-
IT IS STILL NOT A REASON TO RAISE THE DEFAULT. This is a Mac with a lot of cores
|
|
95
|
-
and a fixture whose tiles cost about a millisecond; a phone under thermal
|
|
96
|
-
pressure with a real drawing is a different measurement, and it is the one that
|
|
97
|
-
decides. Printed here so the shape is on record and so a device run has something
|
|
98
|
-
to be compared against.
|
|
99
|
-
*/
|
|
100
|
-
func testLaneScaling() throws {
|
|
101
|
-
let data = TestPdfs.grid(width: 1728, height: 2592, pitch: 4)
|
|
102
|
-
var items: [(Double, Double)] = []
|
|
103
|
-
for index in 0..<24 {
|
|
104
|
-
let column = Double(index % 6) * 256.0
|
|
105
|
-
let row = Double(index / 6) * 256.0
|
|
106
|
-
items.append((column, row))
|
|
107
|
-
}
|
|
108
|
-
var baseline = 0.0
|
|
109
|
-
for lanes in [1, 2, 4] {
|
|
110
|
-
let r = try openFixture(
|
|
111
|
-
data, name: "timing-lanes-\(lanes).pdf", poolSize: lanes)
|
|
112
|
-
let lock = NSLock()
|
|
113
|
-
var completed = 0
|
|
114
|
-
let ms = measure("\(lanes) lane(s), 24x 1024px tiles @4x") {
|
|
115
|
-
var cursor = 0
|
|
116
|
-
DispatchQueue.concurrentPerform(iterations: lanes) { _ in
|
|
117
|
-
while true {
|
|
118
|
-
lock.lock()
|
|
119
|
-
let index = cursor
|
|
120
|
-
cursor += 1
|
|
121
|
-
lock.unlock()
|
|
122
|
-
if index >= items.count { return }
|
|
123
|
-
let (x, y) = items[index]
|
|
124
|
-
if (try? r.render(
|
|
125
|
-
request(x: x, y: y, width: 256, height: 256, scale: 4),
|
|
126
|
-
cancellation: nil, sink: nil)) != nil
|
|
127
|
-
{
|
|
128
|
-
lock.lock()
|
|
129
|
-
completed += 1
|
|
130
|
-
lock.unlock()
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
if lanes == 1 { baseline = ms }
|
|
136
|
-
print(
|
|
137
|
-
"PDFCANVAS-TIMING \(lanes) lane(s) relative: "
|
|
138
|
-
+ String(format: "%.2fx", baseline / ms))
|
|
139
|
-
XCTAssertEqual(
|
|
140
|
-
completed, items.count * 5, "every tile in every repetition must land")
|
|
141
|
-
r.close()
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
/**
|
|
146
|
-
The two engines' relative cost, which is what makes `annotations: true` a
|
|
147
|
-
deliberate choice rather than a free one.
|
|
148
|
-
|
|
149
|
-
PDFKit has to build and keep a `PDFDocument` alongside the `CGPDFDocumentRef`, so
|
|
150
|
-
the FIRST annotated render in a lane pays for a second parse. That is why the
|
|
151
|
-
PDFKit document is lazy — every tile the controller asks for passes
|
|
152
|
-
`annotations: false` and never triggers it.
|
|
153
|
-
*/
|
|
154
|
-
func testEngineCostComparison() throws {
|
|
155
|
-
let r = try openFixture(
|
|
156
|
-
TestPdfs.grid(width: 1728, height: 2592, pitch: 8), name: "timing-engines.pdf")
|
|
157
|
-
var rendered = 0
|
|
158
|
-
// Warm both, so neither measurement includes its parse.
|
|
159
|
-
_ = try? r.render(
|
|
160
|
-
request(width: 200, height: 200, scale: 1, annotations: false),
|
|
161
|
-
cancellation: nil, sink: nil)
|
|
162
|
-
_ = try? r.render(
|
|
163
|
-
request(width: 200, height: 200, scale: 1, annotations: true),
|
|
164
|
-
cancellation: nil, sink: nil)
|
|
165
|
-
|
|
166
|
-
for annotations in [false, true] {
|
|
167
|
-
_ = measure("512px @4x annotations=\(annotations)") {
|
|
168
|
-
if (try? r.render(
|
|
169
|
-
request(width: 128, height: 128, scale: 4, annotations: annotations),
|
|
170
|
-
cancellation: nil, sink: nil)) != nil
|
|
171
|
-
{
|
|
172
|
-
rendered += 1
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
XCTAssertEqual(rendered, 10)
|
|
177
|
-
r.close()
|
|
178
|
-
}
|
|
179
|
-
}
|
|
@@ -1,283 +0,0 @@
|
|
|
1
|
-
import CoreGraphics
|
|
2
|
-
import XCTest
|
|
3
|
-
|
|
4
|
-
@testable import PdfCanvasCore
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
The three affine transforms, checked by mapping corners — no PDF, no bitmap, no
|
|
8
|
-
CoreGraphics state.
|
|
9
|
-
|
|
10
|
-
THIS FILE IS DELIBERATELY FIRST. Every failure mode of a transform produces a
|
|
11
|
-
plausible-looking image rather than an error: a rotation applied the wrong way
|
|
12
|
-
round, a crop-box origin left in, a y-flip half a pixel off. Checking them here
|
|
13
|
-
means a rendering failure further down is a rendering failure, and not this.
|
|
14
|
-
*/
|
|
15
|
-
final class TransformTests: XCTestCase {
|
|
16
|
-
|
|
17
|
-
/// A 200x100 box at the origin, so a swap of width and height is visible.
|
|
18
|
-
private let box = CGRect(x: 0, y: 0, width: 200, height: 100)
|
|
19
|
-
|
|
20
|
-
private func mapped(_ point: CGPoint, _ transform: CGAffineTransform) -> CGPoint {
|
|
21
|
-
point.applying(transform)
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
private func assertPoint(
|
|
25
|
-
_ actual: CGPoint, _ expected: CGPoint, _ what: String,
|
|
26
|
-
file: StaticString = #filePath, line: UInt = #line
|
|
27
|
-
) {
|
|
28
|
-
XCTAssertEqual(
|
|
29
|
-
Double(actual.x), Double(expected.x), accuracy: 1e-9,
|
|
30
|
-
"\(what): x", file: file, line: line)
|
|
31
|
-
XCTAssertEqual(
|
|
32
|
-
Double(actual.y), Double(expected.y), accuracy: 1e-9,
|
|
33
|
-
"\(what): y", file: file, line: line)
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ---------------------------------------------------------------- //
|
|
37
|
-
// PDF user space -> doc space
|
|
38
|
-
// ---------------------------------------------------------------- //
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
Rotation 0 is a pure y-flip through the box's top edge.
|
|
42
|
-
|
|
43
|
-
The PDF's bottom-left corner is doc space's bottom-LEFT, and the PDF's top-left
|
|
44
|
-
is doc (0, 0). An implementation that forgot the flip entirely would map
|
|
45
|
-
bottom-left to (0, 0), which is the single most likely mistake here.
|
|
46
|
-
*/
|
|
47
|
-
func testRotationZeroFlipsYAndNothingElse() {
|
|
48
|
-
let t = PdfCanvasPdfToDocTransform(box, 0)
|
|
49
|
-
assertPoint(mapped(CGPoint(x: 0, y: 100), t), CGPoint(x: 0, y: 0), "PDF top-left")
|
|
50
|
-
assertPoint(
|
|
51
|
-
mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 0, y: 100), "PDF bottom-left")
|
|
52
|
-
assertPoint(
|
|
53
|
-
mapped(CGPoint(x: 200, y: 100), t), CGPoint(x: 200, y: 0), "PDF top-right")
|
|
54
|
-
assertPoint(
|
|
55
|
-
mapped(CGPoint(x: 200, y: 0), t), CGPoint(x: 200, y: 100), "PDF bottom-right")
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
`/Rotate 90` is a CLOCKWISE quarter turn of the displayed page.
|
|
60
|
-
|
|
61
|
-
Turning a sheet of paper clockwise sends its bottom-left corner to the top-left
|
|
62
|
-
and its bottom-right to the bottom-left. Those two are what distinguish 90 from
|
|
63
|
-
270; a test that only checked the page's SIZE would pass for either.
|
|
64
|
-
*/
|
|
65
|
-
func testRotation90TurnsClockwise() {
|
|
66
|
-
let t = PdfCanvasPdfToDocTransform(box, 90)
|
|
67
|
-
assertPoint(
|
|
68
|
-
mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 0, y: 0), "PDF bottom-left")
|
|
69
|
-
assertPoint(
|
|
70
|
-
mapped(CGPoint(x: 200, y: 0), t), CGPoint(x: 0, y: 200), "PDF bottom-right")
|
|
71
|
-
assertPoint(
|
|
72
|
-
mapped(CGPoint(x: 200, y: 100), t), CGPoint(x: 100, y: 200), "PDF top-right")
|
|
73
|
-
assertPoint(
|
|
74
|
-
mapped(CGPoint(x: 0, y: 100), t), CGPoint(x: 100, y: 0), "PDF top-left")
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
func testRotation180TurnsHalfway() {
|
|
78
|
-
let t = PdfCanvasPdfToDocTransform(box, 180)
|
|
79
|
-
assertPoint(
|
|
80
|
-
mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 200, y: 0), "PDF bottom-left")
|
|
81
|
-
assertPoint(
|
|
82
|
-
mapped(CGPoint(x: 0, y: 100), t), CGPoint(x: 200, y: 100), "PDF top-left")
|
|
83
|
-
assertPoint(
|
|
84
|
-
mapped(CGPoint(x: 200, y: 100), t), CGPoint(x: 0, y: 100), "PDF top-right")
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
func testRotation270TurnsCounterClockwise() {
|
|
88
|
-
let t = PdfCanvasPdfToDocTransform(box, 270)
|
|
89
|
-
assertPoint(
|
|
90
|
-
mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 100, y: 200), "PDF bottom-left")
|
|
91
|
-
assertPoint(
|
|
92
|
-
mapped(CGPoint(x: 0, y: 100), t), CGPoint(x: 0, y: 200), "PDF top-left")
|
|
93
|
-
assertPoint(
|
|
94
|
-
mapped(CGPoint(x: 200, y: 100), t), CGPoint(x: 0, y: 0), "PDF top-right")
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
Every rotation is a reflection, not a rotation, in raw coordinates.
|
|
99
|
-
|
|
100
|
-
Doc space is y-down and PDF space is y-up, so a determinant of +1 anywhere here
|
|
101
|
-
would mean the flip had been lost. Cheap, and it catches a whole class of sign
|
|
102
|
-
errors that the corner checks above could miss if two of them were wrong in
|
|
103
|
-
compensating ways.
|
|
104
|
-
*/
|
|
105
|
-
func testEveryRotationHasDeterminantMinusOne() {
|
|
106
|
-
for rotation in [0, 90, 180, 270] {
|
|
107
|
-
let t = PdfCanvasPdfToDocTransform(box, rotation)
|
|
108
|
-
XCTAssertEqual(
|
|
109
|
-
Double(t.a * t.d - t.b * t.c), -1, accuracy: 1e-9,
|
|
110
|
-
"rotation \(rotation) should be orientation-reversing")
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
The crop box's own top-left lands at doc (0, 0), whatever its origin is.
|
|
116
|
-
|
|
117
|
-
The fixture this mirrors has MediaBox `0 0 400 400` and CropBox
|
|
118
|
-
`100 150 300 350`. Without the `-minX` / `minY + height` terms the marker at
|
|
119
|
-
the crop box's top-left would be drawn 100pt right and 50pt down, which on a
|
|
120
|
-
flat page is invisible.
|
|
121
|
-
*/
|
|
122
|
-
func testCropBoxOriginIsSubtracted() {
|
|
123
|
-
let crop = CGRect(x: 100, y: 150, width: 200, height: 200)
|
|
124
|
-
let t = PdfCanvasPdfToDocTransform(crop, 0)
|
|
125
|
-
assertPoint(
|
|
126
|
-
mapped(CGPoint(x: 100, y: 350), t), CGPoint(x: 0, y: 0), "crop top-left")
|
|
127
|
-
assertPoint(
|
|
128
|
-
mapped(CGPoint(x: 300, y: 150), t), CGPoint(x: 200, y: 200),
|
|
129
|
-
"crop bottom-right")
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
/// The same, for a rotated page — where the two corrections interact.
|
|
133
|
-
func testCropBoxOriginIsSubtractedUnderRotation() {
|
|
134
|
-
let crop = CGRect(x: 100, y: 150, width: 200, height: 100)
|
|
135
|
-
// Rotated 90, the page is 100 wide and 200 tall.
|
|
136
|
-
let t = PdfCanvasPdfToDocTransform(crop, 90)
|
|
137
|
-
assertPoint(
|
|
138
|
-
mapped(CGPoint(x: 100, y: 150), t), CGPoint(x: 0, y: 0),
|
|
139
|
-
"crop bottom-left -> doc top-left")
|
|
140
|
-
assertPoint(
|
|
141
|
-
mapped(CGPoint(x: 300, y: 150), t), CGPoint(x: 0, y: 200),
|
|
142
|
-
"crop bottom-right -> doc bottom-left")
|
|
143
|
-
assertPoint(
|
|
144
|
-
mapped(CGPoint(x: 100, y: 250), t), CGPoint(x: 100, y: 0),
|
|
145
|
-
"crop top-left -> doc top-right")
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
/// A rotation that is not a multiple of 90 is treated as 0 rather than rounded.
|
|
149
|
-
func testUnusableRotationsFallBackToZero() {
|
|
150
|
-
let zero = PdfCanvasPdfToDocTransform(box, 0)
|
|
151
|
-
for rotation in [45, 1, 359, 137] {
|
|
152
|
-
XCTAssertTrue(
|
|
153
|
-
PdfCanvasPdfToDocTransform(box, rotation) == zero,
|
|
154
|
-
"rotation \(rotation) should behave as 0")
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
/// Negatives and out-of-range multiples normalise rather than falling back.
|
|
159
|
-
func testRotationsNormalise() {
|
|
160
|
-
XCTAssertEqual(PdfCanvasNormalizeRotation(-90), 270)
|
|
161
|
-
XCTAssertEqual(PdfCanvasNormalizeRotation(450), 90)
|
|
162
|
-
XCTAssertEqual(PdfCanvasNormalizeRotation(-450), 270)
|
|
163
|
-
XCTAssertEqual(PdfCanvasNormalizeRotation(720), 0)
|
|
164
|
-
XCTAssertEqual(PdfCanvasNormalizeRotation(45), 0)
|
|
165
|
-
XCTAssertTrue(
|
|
166
|
-
PdfCanvasPdfToDocTransform(box, -90) == PdfCanvasPdfToDocTransform(box, 270),
|
|
167
|
-
"-90 should behave as 270")
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
// ---------------------------------------------------------------- //
|
|
171
|
-
// Doc space -> raster space
|
|
172
|
-
// ---------------------------------------------------------------- //
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
The doc rect's top-left lands at the raster's top-left, and its bottom-right at
|
|
176
|
-
the bottom-right.
|
|
177
|
-
|
|
178
|
-
Stated in CG's own y-up coordinates, because that is what the transform emits:
|
|
179
|
-
the raster's top-left is (0, H).
|
|
180
|
-
*/
|
|
181
|
-
func testDocRectMapsOntoTheWholeRaster() {
|
|
182
|
-
let t = PdfCanvasDocToRasterTransform(37, 23, 4, 164)
|
|
183
|
-
assertPoint(
|
|
184
|
-
mapped(CGPoint(x: 37, y: 23), t), CGPoint(x: 0, y: 164), "doc top-left")
|
|
185
|
-
assertPoint(
|
|
186
|
-
mapped(CGPoint(x: 37 + 64, y: 23 + 41), t), CGPoint(x: 256, y: 0),
|
|
187
|
-
"doc bottom-right")
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
TWO RASTERS AT THE SAME SCALE AGREE WHERE THEY OVERLAP — the property the cache
|
|
192
|
-
and the overlapping detail pieces depend on, and the one the rounded
|
|
193
|
-
`pixelHeight` in the flip looks like it should break.
|
|
194
|
-
*
|
|
195
|
-
The two rects below have heights that round in OPPOSITE directions at scale 1
|
|
196
|
-
(100.4 -> 100, 100.6 -> 101), so a transform that leaked its rounding into
|
|
197
|
-
content placement would put the same doc point on different rows. The check is
|
|
198
|
-
in ROWS FROM THE TOP, which is how a raster is indexed and where the `H` term
|
|
199
|
-
cancels.
|
|
200
|
-
*/
|
|
201
|
-
func testRastersAtTheSameScaleAgreeWhereTheyOverlap() {
|
|
202
|
-
let scale = 1.0
|
|
203
|
-
// Rect A: doc y 0 .. 100.4, 100 rows. Rect B: doc y 50 .. 150.6, 101 rows.
|
|
204
|
-
let a = PdfCanvasDocToRasterTransform(0, 0, scale, 100)
|
|
205
|
-
let b = PdfCanvasDocToRasterTransform(0, 50, scale, 101)
|
|
206
|
-
|
|
207
|
-
for docY in stride(from: 50.0, through: 100.0, by: 5.0) {
|
|
208
|
-
let rowFromTopInA = 100 - Double(mapped(CGPoint(x: 0, y: docY), a).y)
|
|
209
|
-
let rowFromTopInB = 101 - Double(mapped(CGPoint(x: 0, y: docY), b).y)
|
|
210
|
-
XCTAssertEqual(
|
|
211
|
-
rowFromTopInA, docY, accuracy: 1e-9,
|
|
212
|
-
"doc y \(docY) should sit at row \(docY) of A")
|
|
213
|
-
XCTAssertEqual(
|
|
214
|
-
rowFromTopInB, docY - 50, accuracy: 1e-9,
|
|
215
|
-
"doc y \(docY) should sit at row \(docY - 50) of B")
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
/**
|
|
220
|
-
Scale is used exactly as requested, never re-derived from the pixel size.
|
|
221
|
-
|
|
222
|
-
A doc rect of 512.4 points at scale 4 is 2050 pixels wide (2049.6 rounded), and
|
|
223
|
-
a re-derived scale would be 2050/512.4 = 4.00078. Over 2050 pixels that is a
|
|
224
|
-
drift of 1.6 px at the far edge relative to a neighbour drawn at exactly 4 —
|
|
225
|
-
which is precisely the misregistration the overlap rule cannot absorb.
|
|
226
|
-
*/
|
|
227
|
-
func testScaleIsNotReDerivedFromThePixelSize() {
|
|
228
|
-
let scale = 4.0
|
|
229
|
-
let docWidth = 512.4
|
|
230
|
-
let r = request(width: docWidth, height: 100, scale: scale)
|
|
231
|
-
XCTAssertEqual(r.pixelWidth, 2050, "512.4 * 4 = 2049.6, rounds to 2050")
|
|
232
|
-
|
|
233
|
-
let t = PdfCanvasDocToRasterTransform(0, 0, scale, r.pixelHeight)
|
|
234
|
-
// The far edge lands 0.4 px short of the raster's right edge — that is the
|
|
235
|
-
// half-pixel the comment on `tileMatrix` describes, left showing the fill.
|
|
236
|
-
XCTAssertEqual(
|
|
237
|
-
Double(mapped(CGPoint(x: docWidth, y: 0), t).x), 2049.6, accuracy: 1e-9)
|
|
238
|
-
// A re-derived scale would have landed it at exactly 2050, agreeing with the
|
|
239
|
-
// raster and disagreeing with every neighbour.
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
// ---------------------------------------------------------------- //
|
|
243
|
-
// Doc space -> PDFKit's space
|
|
244
|
-
// ---------------------------------------------------------------- //
|
|
245
|
-
|
|
246
|
-
/**
|
|
247
|
-
PDFKit's transform is a pure flip through the POST-ROTATION page height.
|
|
248
|
-
|
|
249
|
-
Nothing else, because PDFKit applies `/Rotate` and the crop-box origin itself —
|
|
250
|
-
measured, and the asymmetry with `-[PDFPage boundsForBox:]` is the trap this
|
|
251
|
-
whole function exists to keep out of the rasterizer.
|
|
252
|
-
*/
|
|
253
|
-
func testPdfKitTransformIsAPureFlip() {
|
|
254
|
-
let t = PdfCanvasDocToPdfKitTransform(612)
|
|
255
|
-
assertPoint(mapped(CGPoint(x: 0, y: 0), t), CGPoint(x: 0, y: 612), "doc origin")
|
|
256
|
-
assertPoint(
|
|
257
|
-
mapped(CGPoint(x: 792, y: 612), t), CGPoint(x: 792, y: 0),
|
|
258
|
-
"doc bottom-right")
|
|
259
|
-
XCTAssertEqual(Double(t.a * t.d - t.b * t.c), -1, accuracy: 1e-9)
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// ---------------------------------------------------------------- //
|
|
263
|
-
// Pixel sizing
|
|
264
|
-
// ---------------------------------------------------------------- //
|
|
265
|
-
|
|
266
|
-
/**
|
|
267
|
-
`round`, at least 1, matching `planner.ts` and `RasterRequest.java`.
|
|
268
|
-
|
|
269
|
-
The three have to agree exactly: the planner declares the pixel size in the
|
|
270
|
-
`RasterPlanItem` the cache is keyed on, so a backend that used `ceil` would
|
|
271
|
-
return a raster of a different size than the plan promised.
|
|
272
|
-
*/
|
|
273
|
-
func testPixelExtentsRoundAndFloorAtOne() {
|
|
274
|
-
XCTAssertEqual(request(width: 100, height: 100, scale: 1).pixelWidth, 100)
|
|
275
|
-
XCTAssertEqual(request(width: 100.4, height: 1, scale: 1).pixelWidth, 100)
|
|
276
|
-
XCTAssertEqual(request(width: 100.5, height: 1, scale: 1).pixelWidth, 101)
|
|
277
|
-
XCTAssertEqual(request(width: 100.6, height: 1, scale: 1).pixelWidth, 101)
|
|
278
|
-
XCTAssertEqual(request(width: 0.1, height: 0.1, scale: 1).pixelWidth, 1)
|
|
279
|
-
XCTAssertEqual(request(width: 0.1, height: 0.1, scale: 1).pixelHeight, 1)
|
|
280
|
-
XCTAssertEqual(request(width: 512.4, height: 1, scale: 4).pixelWidth, 2050)
|
|
281
|
-
XCTAssertEqual(request(width: 512.5, height: 1, scale: 4).pixelWidth, 2050)
|
|
282
|
-
}
|
|
283
|
-
}
|