@reekon-tools/react-native-pdf-canvas 0.1.1 → 0.2.0
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 +68 -39
- package/README.md +213 -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 +487 -0
- package/scripts/pdfium-manifest.json +46 -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,45 +0,0 @@
|
|
|
1
|
-
package tools.reekon.pdfcanvas;
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* A page's intrinsic size in PDF points, mirroring {@code PageGeometry} in
|
|
5
|
-
* {@code src/types.ts}.
|
|
6
|
-
*
|
|
7
|
-
* POST-ROTATION, per that contract. {@code PdfRenderer.Page.getWidth()} already
|
|
8
|
-
* is: it forwards to PDFium's {@code FPDF_GetPageWidth}, and
|
|
9
|
-
* {@code CPDF_Page::UpdateDimensions} swaps the media box's width and height
|
|
10
|
-
* when {@code /Rotate} is 90 or 270. So a 612x792 page carrying {@code /Rotate 90}
|
|
11
|
-
* reports 792x612, and the render matrix operates in that same upright space —
|
|
12
|
-
* which is why the tiling arithmetic never has to know about rotation at all.
|
|
13
|
-
* {@code pageGeometryIsPostRotation} measures this rather than trusting it.
|
|
14
|
-
*
|
|
15
|
-
* {@link #rotationDegrees} is therefore reported as 0, NOT as the page's
|
|
16
|
-
* {@code /Rotate} value: the platform pre-applies the rotation to both the
|
|
17
|
-
* dimensions and the transform, so from this backend's vantage point there is no
|
|
18
|
-
* residual rotation left to apply. There is also no way to read {@code /Rotate}
|
|
19
|
-
* through {@code PdfRenderer} below API 35, so a non-zero value here could only
|
|
20
|
-
* be invented. Reporting the truth of what the caller must do — nothing — beats
|
|
21
|
-
* reporting a number the caller would then wrongly compensate for.
|
|
22
|
-
*/
|
|
23
|
-
public final class PageGeometry {
|
|
24
|
-
|
|
25
|
-
public final int index;
|
|
26
|
-
/** Width in PDF points, after /Rotate. */
|
|
27
|
-
public final int widthPoints;
|
|
28
|
-
/** Height in PDF points, after /Rotate. */
|
|
29
|
-
public final int heightPoints;
|
|
30
|
-
/** Residual rotation the caller must still apply. Always 0 here; see above. */
|
|
31
|
-
public final int rotationDegrees;
|
|
32
|
-
|
|
33
|
-
PageGeometry(int index, int widthPoints, int heightPoints, int rotationDegrees) {
|
|
34
|
-
this.index = index;
|
|
35
|
-
this.widthPoints = widthPoints;
|
|
36
|
-
this.heightPoints = heightPoints;
|
|
37
|
-
this.rotationDegrees = rotationDegrees;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
@Override
|
|
41
|
-
public String toString() {
|
|
42
|
-
return "PageGeometry{" + index + " " + widthPoints + "x" + heightPoints
|
|
43
|
-
+ "pt rot=" + rotationDegrees + "}";
|
|
44
|
-
}
|
|
45
|
-
}
|
|
@@ -1,564 +0,0 @@
|
|
|
1
|
-
package tools.reekon.pdfcanvas;
|
|
2
|
-
|
|
3
|
-
import android.graphics.Bitmap;
|
|
4
|
-
import android.graphics.Color;
|
|
5
|
-
import android.graphics.Matrix;
|
|
6
|
-
import android.graphics.pdf.PdfRenderer;
|
|
7
|
-
|
|
8
|
-
import java.io.Closeable;
|
|
9
|
-
import java.io.File;
|
|
10
|
-
import java.io.FileOutputStream;
|
|
11
|
-
import java.io.IOException;
|
|
12
|
-
import java.nio.ByteBuffer;
|
|
13
|
-
|
|
14
|
-
import tools.reekon.pdfcanvas.jsi.PdfCanvasPixelBridge;
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* The Android page rasterizer. Plain Android — {@code PdfRenderer},
|
|
18
|
-
* {@code Bitmap}, {@code ParcelFileDescriptor} — and NOTHING from React Native
|
|
19
|
-
* or Nitro, so it can be proven on a bare emulator with
|
|
20
|
-
* {@code ./gradlew connectedDebugAndroidTest} and no app around it.
|
|
21
|
-
*
|
|
22
|
-
* Shaped to serve {@code RasterizerHandle} in {@code src/types.ts}:
|
|
23
|
-
* {@code pageCount}, {@code pageGeometry(index)}, {@code render(request, signal)},
|
|
24
|
-
* {@code close()}. The binding in {@code src/reactnative} is a marshalling layer
|
|
25
|
-
* over this and adds no behaviour.
|
|
26
|
-
*
|
|
27
|
-
* <h3>How a tile is drawn</h3>
|
|
28
|
-
*
|
|
29
|
-
* {@code setScale(s, s); postTranslate(-x*s, -y*s)} — spike S1, finding 1,
|
|
30
|
-
* MEASURED on API 24, 31 and 33: a tile rendered with that matrix is
|
|
31
|
-
* pixel-identical to the same region cropped out of a full-page render
|
|
32
|
-
* (0/262144 mismatching pixels, max channel delta 1) on all three. A translate
|
|
33
|
-
* that puts the page origin OUTSIDE the destination bitmap is honoured exactly,
|
|
34
|
-
* which is the only reason tiling on the platform renderer is possible at all;
|
|
35
|
-
* had it failed, Android would have gone straight to vendored PDFium.
|
|
36
|
-
*
|
|
37
|
-
* S1 finding 2, also measured: cost tracks DESTINATION BITMAP AREA, not page
|
|
38
|
-
* area or scale. An ARCH-D tile at 8x costs the same as at 1x, so the engine
|
|
39
|
-
* clips rather than rasterizing the page and discarding most of it. That is what
|
|
40
|
-
* makes a viewport-sized detail raster constant in zoom.
|
|
41
|
-
*/
|
|
42
|
-
public final class PdfCanvasRasterizer implements Closeable {
|
|
43
|
-
|
|
44
|
-
/** Surfaced as {@code PageRasterizer.id}, so a bug report names the backend. */
|
|
45
|
-
public static final String ID = "android-pdfrenderer";
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Refuse absurd rasters instead of dying on {@code OutOfMemoryError}.
|
|
49
|
-
*
|
|
50
|
-
* 64 Mpx is 256 MB at 4 bytes per pixel — already past what a mid-tier device
|
|
51
|
-
* will hand out in one allocation, so anything above it is a planner bug, and
|
|
52
|
-
* a named error against a raster key is a far better diagnostic than a process
|
|
53
|
-
* death with no key attached. The TS policy has its own, much lower, ceilings
|
|
54
|
-
* ({@code maxRasterPixels}); this is the backstop for a request that reaches
|
|
55
|
-
* here anyway.
|
|
56
|
-
*/
|
|
57
|
-
public static final int MAX_RASTER_PIXELS = 64_000_000;
|
|
58
|
-
|
|
59
|
-
private final PdfRendererPool pool;
|
|
60
|
-
private final PageGeometry[] pages;
|
|
61
|
-
private final File spooled;
|
|
62
|
-
private volatile boolean closed;
|
|
63
|
-
|
|
64
|
-
private PdfCanvasRasterizer(PdfRendererPool pool, PageGeometry[] pages, File spooled) {
|
|
65
|
-
this.pool = pool;
|
|
66
|
-
this.pages = pages;
|
|
67
|
-
this.spooled = spooled;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/* ---------------------------------------------------------------- *
|
|
71
|
-
* Opening
|
|
72
|
-
* ---------------------------------------------------------------- */
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* @param password must be null. Kept in the signature because the TS
|
|
76
|
-
* {@code PdfSource} carries one and dropping it here would make the
|
|
77
|
-
* backend silently ignore it; {@link PdfRendererPool#open} rejects a
|
|
78
|
-
* non-null value with {@code unsupported} and says why.
|
|
79
|
-
* @param poolSize number of concurrent renders to allow, clamped to
|
|
80
|
-
* [1, {@link PdfRendererPool#MAX_POOL_SIZE}].
|
|
81
|
-
*/
|
|
82
|
-
public static PdfCanvasRasterizer openFile(File file, String password, int poolSize)
|
|
83
|
-
throws PdfRasterException {
|
|
84
|
-
return open(file, password, poolSize, null);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Spools bytes to a file first, because {@code PdfRenderer} requires a
|
|
89
|
-
* SEEKABLE descriptor — it reads through a seek-and-read callback, so a pipe
|
|
90
|
-
* or a socket is not an option and neither is anything streamed. The spooled
|
|
91
|
-
* file is deleted by {@link #close()}.
|
|
92
|
-
*/
|
|
93
|
-
public static PdfCanvasRasterizer openBytes(
|
|
94
|
-
File spoolDir, byte[] data, String password, int poolSize) throws PdfRasterException {
|
|
95
|
-
if (data == null || data.length == 0) {
|
|
96
|
-
throw new PdfRasterException(PdfErrorCode.CORRUPT, "Document data is empty.");
|
|
97
|
-
}
|
|
98
|
-
File spool;
|
|
99
|
-
try {
|
|
100
|
-
if (!spoolDir.exists() && !spoolDir.mkdirs()) {
|
|
101
|
-
throw new IOException("Could not create " + spoolDir.getAbsolutePath());
|
|
102
|
-
}
|
|
103
|
-
spool = File.createTempFile("pdfcanvas-", ".pdf", spoolDir);
|
|
104
|
-
try (FileOutputStream out = new FileOutputStream(spool)) {
|
|
105
|
-
out.write(data);
|
|
106
|
-
}
|
|
107
|
-
} catch (IOException e) {
|
|
108
|
-
throw new PdfRasterException(
|
|
109
|
-
PdfErrorCode.BACKEND_FAILURE, "Could not spool document data to disk.", e);
|
|
110
|
-
}
|
|
111
|
-
try {
|
|
112
|
-
return open(spool, password, poolSize, spool);
|
|
113
|
-
} catch (PdfRasterException e) {
|
|
114
|
-
// The spool is ours; nothing else will ever delete it if open() fails.
|
|
115
|
-
//noinspection ResultOfMethodCallIgnored
|
|
116
|
-
spool.delete();
|
|
117
|
-
throw e;
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
private static PdfCanvasRasterizer open(
|
|
122
|
-
File file, String password, int poolSize, File spooled) throws PdfRasterException {
|
|
123
|
-
PdfRendererPool pool = PdfRendererPool.open(file, password, poolSize);
|
|
124
|
-
try {
|
|
125
|
-
// Every page's geometry is probed ONCE, here. The contract says
|
|
126
|
-
// pageGeometry() is cheap and "must not load a page", and the only way to
|
|
127
|
-
// learn a page's size through PdfRenderer is to open it — so the loading
|
|
128
|
-
// happens at open() and pageGeometry() becomes an array read.
|
|
129
|
-
// pageGeometryIsPrecomputedAtOpenNotLoadedOnDemand pins that by asking for
|
|
130
|
-
// geometry AFTER close(), when every PdfRenderer is shut: a lazy
|
|
131
|
-
// implementation would throw or crash there, and it reads a page it never
|
|
132
|
-
// asked about before the close so a lazily-cached page 0 cannot pass by
|
|
133
|
-
// accident. (This comment previously named a pageGeometryDoesNotOpenAPage
|
|
134
|
-
// test, which does not exist, and described a leased-out-pool setup that
|
|
135
|
-
// is not what the real test does.)
|
|
136
|
-
PageGeometry[] pages = probeGeometry(pool);
|
|
137
|
-
return new PdfCanvasRasterizer(pool, pages, spooled);
|
|
138
|
-
} catch (PdfRasterException | RuntimeException e) {
|
|
139
|
-
pool.close();
|
|
140
|
-
throw e;
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
private static PageGeometry[] probeGeometry(PdfRendererPool pool)
|
|
145
|
-
throws PdfRasterException {
|
|
146
|
-
int count = pool.pageCount();
|
|
147
|
-
PageGeometry[] pages = new PageGeometry[count];
|
|
148
|
-
PdfRenderer renderer = pool.lease();
|
|
149
|
-
try {
|
|
150
|
-
for (int i = 0; i < count; i++) {
|
|
151
|
-
PdfRenderer.Page page = null;
|
|
152
|
-
try {
|
|
153
|
-
page = renderer.openPage(i);
|
|
154
|
-
// getWidth()/getHeight() are already post-/Rotate; see PageGeometry.
|
|
155
|
-
pages[i] = new PageGeometry(i, page.getWidth(), page.getHeight(), 0);
|
|
156
|
-
} catch (RuntimeException e) {
|
|
157
|
-
throw new PdfRasterException(
|
|
158
|
-
PdfErrorCode.CORRUPT, "Could not read page " + i + ".", e);
|
|
159
|
-
} finally {
|
|
160
|
-
if (page != null) {
|
|
161
|
-
page.close();
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
} finally {
|
|
166
|
-
pool.release(renderer);
|
|
167
|
-
}
|
|
168
|
-
return pages;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/* ---------------------------------------------------------------- *
|
|
172
|
-
* Cheap accessors
|
|
173
|
-
* ---------------------------------------------------------------- */
|
|
174
|
-
|
|
175
|
-
public int pageCount() {
|
|
176
|
-
return pages.length;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/** An array read. See the note in {@link #open}. */
|
|
180
|
-
public PageGeometry pageGeometry(int index) throws PdfRasterException {
|
|
181
|
-
if (index < 0 || index >= pages.length) {
|
|
182
|
-
throw new PdfRasterException(
|
|
183
|
-
PdfErrorCode.NOT_FOUND,
|
|
184
|
-
"Document has " + pages.length + " pages; asked for " + index + ".");
|
|
185
|
-
}
|
|
186
|
-
return pages[index];
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
/** How many renders can genuinely be in flight at once. */
|
|
190
|
-
public int poolSize() {
|
|
191
|
-
return pool.size();
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
/**
|
|
195
|
-
* Whether this device draws a page carrying {@code /Rotate} upright.
|
|
196
|
-
*
|
|
197
|
-
* NOT a formality: on API 24 it does not — the dimensions swap but the content
|
|
198
|
-
* is drawn unrotated and clipped, so a rotated page comes back wrong with no
|
|
199
|
-
* error anywhere. There is nothing this backend can do about it, because
|
|
200
|
-
* {@code PdfRenderer} exposes no way to read a page's {@code /Rotate} value
|
|
201
|
-
* below API 35 and so no way to compensate. What it CAN do is say so, which is
|
|
202
|
-
* what this is for: a host can fall back to another backend, or warn, instead
|
|
203
|
-
* of silently showing a sheared drawing. See {@link PdfPlatformQuirks}.
|
|
204
|
-
*/
|
|
205
|
-
public static PdfPlatformQuirks.RotationSupport rotationSupport(File scratchDir) {
|
|
206
|
-
return PdfPlatformQuirks.rotationSupport(scratchDir);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/* ---------------------------------------------------------------- *
|
|
210
|
-
* Rendering
|
|
211
|
-
* ---------------------------------------------------------------- */
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* The heap form: the pixels come back in {@code RasterPixels.bytes}.
|
|
215
|
-
*
|
|
216
|
-
* This is what every caller that wants to READ pixels uses, and it is the only
|
|
217
|
-
* form the instrumented suite exercises for correctness.
|
|
218
|
-
*/
|
|
219
|
-
public RasterPixels render(RasterRequest request, RasterCancellation signal)
|
|
220
|
-
throws PdfRasterException {
|
|
221
|
-
return render(request, signal, RasterPixels.NO_SLOT);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
/**
|
|
225
|
-
* The one-copy form: the finished raster is written straight into native
|
|
226
|
-
* memory under {@code slot} and never touches the Java heap.
|
|
227
|
-
*
|
|
228
|
-
* DELETES, per tile, one {@code new byte[byteCount]} on ART's large-object
|
|
229
|
-
* space and one full-raster memcpy out of it. What is left is the single copy
|
|
230
|
-
* out of the {@code Bitmap}'s own pixels, which the heap form pays as well.
|
|
231
|
-
*
|
|
232
|
-
* <h3>The result may be heap-backed anyway, and callers must handle that</h3>
|
|
233
|
-
*
|
|
234
|
-
* {@code PdfCanvasPixelBridge.nativeStoreFromBitmap} is allowed to REFUSE — no
|
|
235
|
-
* native memory to be had, a stride the NDK reports differently from the
|
|
236
|
-
* Bitmap, a slot id already occupied — and every refusal falls through to the
|
|
237
|
-
* heap path, because a slower correct tile beats a lost one. So check
|
|
238
|
-
* {@link RasterPixels#isInNativeSlot()} on the way out rather than assuming.
|
|
239
|
-
* When it is false, {@code slot} holds nothing and there is nothing to release.
|
|
240
|
-
*
|
|
241
|
-
* <h3>What is released, and where</h3>
|
|
242
|
-
*
|
|
243
|
-
* Nothing, on the happy path: the slot is taken exactly once by JS. This
|
|
244
|
-
* method DOES release it on the one abandon path it owns — a cancellation
|
|
245
|
-
* observed after the draw finished, which is the post-render check in
|
|
246
|
-
* {@link #renderLeased}. Every abandon path AFTER this method returns belongs
|
|
247
|
-
* to the caller, and {@code PdfCanvasModule.render} closes them with a
|
|
248
|
-
* {@code finally} that releases unless the slot was handed to the promise.
|
|
249
|
-
*
|
|
250
|
-
* @param slot a slot id from a per-process monotonic counter. Must not be
|
|
251
|
-
* {@link RasterPixels#NO_SLOT}, and must not be an id already in use — the
|
|
252
|
-
* native side refuses a collision rather than overwriting, since an
|
|
253
|
-
* overwrite hands one raster's memory to another raster's take.
|
|
254
|
-
*/
|
|
255
|
-
public RasterPixels renderToNativeSlot(
|
|
256
|
-
RasterRequest request, RasterCancellation signal, long slot)
|
|
257
|
-
throws PdfRasterException {
|
|
258
|
-
if (slot == RasterPixels.NO_SLOT) {
|
|
259
|
-
throw new PdfRasterException(
|
|
260
|
-
PdfErrorCode.BACKEND_FAILURE,
|
|
261
|
-
"renderToNativeSlot needs a real slot id; " + RasterPixels.NO_SLOT
|
|
262
|
-
+ " is reserved for heap-backed rasters.");
|
|
263
|
-
}
|
|
264
|
-
return render(request, signal, slot);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
private RasterPixels render(RasterRequest request, RasterCancellation signal, long slot)
|
|
268
|
-
throws PdfRasterException {
|
|
269
|
-
RasterCancellation.throwIfCancelled(signal, "before start");
|
|
270
|
-
requireOpen();
|
|
271
|
-
|
|
272
|
-
// Validates the page index before anything expensive is allocated or leased.
|
|
273
|
-
pageGeometry(request.page);
|
|
274
|
-
|
|
275
|
-
if (!(request.scale > 0f) || Float.isInfinite(request.scale)) {
|
|
276
|
-
throw new PdfRasterException(
|
|
277
|
-
PdfErrorCode.BACKEND_FAILURE, "Scale must be finite and positive: " + request.scale);
|
|
278
|
-
}
|
|
279
|
-
if (!(request.docWidth > 0f) || !(request.docHeight > 0f)) {
|
|
280
|
-
throw new PdfRasterException(
|
|
281
|
-
PdfErrorCode.BACKEND_FAILURE,
|
|
282
|
-
"Doc rect has no area: " + request.docWidth + "x" + request.docHeight);
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
int pxW = request.pixelWidth();
|
|
286
|
-
int pxH = request.pixelHeight();
|
|
287
|
-
long pixels = (long) pxW * (long) pxH;
|
|
288
|
-
if (pixels > MAX_RASTER_PIXELS) {
|
|
289
|
-
throw new PdfRasterException(
|
|
290
|
-
PdfErrorCode.OUT_OF_MEMORY,
|
|
291
|
-
"Refused a " + pxW + "x" + pxH + " raster (" + pixels + " pixels > "
|
|
292
|
-
+ MAX_RASTER_PIXELS + ").");
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
PdfRenderer renderer = pool.lease();
|
|
296
|
-
// The lease can block for the length of another render, so this is the check
|
|
297
|
-
// that actually saves work when a raster is superseded mid-queue.
|
|
298
|
-
try {
|
|
299
|
-
RasterCancellation.throwIfCancelled(signal, "after pool wait");
|
|
300
|
-
requireOpen();
|
|
301
|
-
return renderLeased(renderer, request, pxW, pxH, signal, slot);
|
|
302
|
-
} finally {
|
|
303
|
-
pool.release(renderer);
|
|
304
|
-
}
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
private RasterPixels renderLeased(
|
|
308
|
-
PdfRenderer renderer,
|
|
309
|
-
RasterRequest request,
|
|
310
|
-
int pxW,
|
|
311
|
-
int pxH,
|
|
312
|
-
RasterCancellation signal,
|
|
313
|
-
long slot)
|
|
314
|
-
throws PdfRasterException {
|
|
315
|
-
PdfRenderer.Page page = null;
|
|
316
|
-
Bitmap bitmap = null;
|
|
317
|
-
try {
|
|
318
|
-
page = renderer.openPage(request.page);
|
|
319
|
-
bitmap = createBitmap(pxW, pxH);
|
|
320
|
-
|
|
321
|
-
// S1 finding 3, MEASURED: render() does NOT clear the destination. A
|
|
322
|
-
// bitmap arrives from createBitmap already zeroed, so 'transparent' would
|
|
323
|
-
// survive without this call — but writing it explicitly is what makes the
|
|
324
|
-
// two backgrounds one decision instead of one decision and one accident,
|
|
325
|
-
// and it is required for the reuse path the moment bitmaps get pooled.
|
|
326
|
-
bitmap.eraseColor(
|
|
327
|
-
request.background == RasterRequest.BACKGROUND_TRANSPARENT
|
|
328
|
-
? Color.TRANSPARENT
|
|
329
|
-
: Color.WHITE);
|
|
330
|
-
|
|
331
|
-
Matrix matrix = tileMatrix(request);
|
|
332
|
-
|
|
333
|
-
RasterCancellation.throwIfCancelled(signal, "before render");
|
|
334
|
-
// Uninterruptible from here to the next statement. See RasterCancellation.
|
|
335
|
-
page.render(bitmap, null, matrix, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
|
|
336
|
-
|
|
337
|
-
RasterPixels out = extract(bitmap, pxW, pxH, slot);
|
|
338
|
-
|
|
339
|
-
// Post-check. This saves no drawing — the pixels above are already
|
|
340
|
-
// finished — it only stops a superseded raster being handed back and
|
|
341
|
-
// ingested into Skia, which is the expensive half on the JS side.
|
|
342
|
-
//
|
|
343
|
-
// THE RELEASE IS NOT OPTIONAL. `out` is dropped on the floor when this
|
|
344
|
-
// throws, and a heap-backed raster is simply collected — but a
|
|
345
|
-
// slot-backed one has already been published to the native map, where
|
|
346
|
-
// nothing else will ever look for it. A superseded epoch produces a
|
|
347
|
-
// stream of these, so leaking here is megabytes per gesture, freed only
|
|
348
|
-
// by the next reload. The check stays AFTER extract rather than moving
|
|
349
|
-
// in front of it so that the cancel window it covers does not shrink.
|
|
350
|
-
try {
|
|
351
|
-
RasterCancellation.throwIfCancelled(signal, "after render");
|
|
352
|
-
} catch (PdfRasterException e) {
|
|
353
|
-
out.release();
|
|
354
|
-
throw e;
|
|
355
|
-
}
|
|
356
|
-
return out;
|
|
357
|
-
} catch (IllegalStateException e) {
|
|
358
|
-
// openPage's documented failure when a page is already open on this
|
|
359
|
-
// renderer. Reaching it means the lease discipline broke, so say that
|
|
360
|
-
// rather than reporting a generic backend failure.
|
|
361
|
-
throw new PdfRasterException(
|
|
362
|
-
PdfErrorCode.BACKEND_FAILURE,
|
|
363
|
-
"PdfRenderer refused page " + request.page
|
|
364
|
-
+ " — a page was already open on this renderer, which means the "
|
|
365
|
-
+ "pool leased it twice.",
|
|
366
|
-
e);
|
|
367
|
-
} catch (IllegalArgumentException e) {
|
|
368
|
-
throw new PdfRasterException(
|
|
369
|
-
PdfErrorCode.NOT_FOUND, "No page " + request.page + " in this document.", e);
|
|
370
|
-
} finally {
|
|
371
|
-
if (bitmap != null) {
|
|
372
|
-
// The pixels have been copied out; holding the Bitmap alive until the GC
|
|
373
|
-
// notices costs pxW*pxH*4 of native heap per raster in flight.
|
|
374
|
-
bitmap.recycle();
|
|
375
|
-
}
|
|
376
|
-
if (page != null) {
|
|
377
|
-
page.close();
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
/**
|
|
383
|
-
* The S1 matrix. {@code scale} is used EXACTLY as requested rather than being
|
|
384
|
-
* re-derived from the rounded pixel size ({@code pxW / docWidth}).
|
|
385
|
-
*
|
|
386
|
-
* That choice matters: it is what makes two rasters requested at the same
|
|
387
|
-
* scale agree pixel-for-pixel wherever they overlap, which is the property the
|
|
388
|
-
* cache and the overlapping detail pieces depend on. Re-deriving would instead
|
|
389
|
-
* make each raster cover its doc rect to the last sub-pixel, at the cost of
|
|
390
|
-
* every pair of neighbours being drawn at imperceptibly different scales. At
|
|
391
|
-
* most half a pixel of the far edge goes undrawn, and it is left showing the
|
|
392
|
-
* background fill.
|
|
393
|
-
*
|
|
394
|
-
* PINNED BY {@code rastersAtTheSameScaleAgreeWhereTheyOverlap}, which asks for
|
|
395
|
-
* two rects at 4x whose pixel widths round in OPPOSITE directions (512.5 up to
|
|
396
|
-
* 513, 512.4 down to 512) and requires them to agree over the overlap. NOT by
|
|
397
|
-
* the S1 equivalence test, which this comment used to name: its rects are 400
|
|
398
|
-
* and 128 points at 4x, both landing on exact pixel counts, so a re-derived
|
|
399
|
-
* scale comes out to exactly 4 for both and that test cannot tell the two
|
|
400
|
-
* implementations apart.
|
|
401
|
-
*/
|
|
402
|
-
private static Matrix tileMatrix(RasterRequest request) {
|
|
403
|
-
Matrix matrix = new Matrix();
|
|
404
|
-
matrix.setScale(request.scale, request.scale);
|
|
405
|
-
matrix.postTranslate(-request.docX * request.scale, -request.docY * request.scale);
|
|
406
|
-
return matrix;
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
private static Bitmap createBitmap(int pxW, int pxH) throws PdfRasterException {
|
|
410
|
-
try {
|
|
411
|
-
// MUTABLE ARGB_8888 is not a preference, it is the platform's rule:
|
|
412
|
-
// PdfRenderer.render throws IllegalArgumentException on an immutable
|
|
413
|
-
// bitmap and on any config other than ARGB_8888. createBitmap returns a
|
|
414
|
-
// mutable bitmap, so there is nothing further to assert here.
|
|
415
|
-
return Bitmap.createBitmap(pxW, pxH, Bitmap.Config.ARGB_8888);
|
|
416
|
-
} catch (OutOfMemoryError e) {
|
|
417
|
-
// Caught, not propagated. An OOM on one oversized raster is recoverable —
|
|
418
|
-
// the cache drops it and the planner asks for less — whereas letting the
|
|
419
|
-
// Error escape takes the whole JS thread's promise chain with it.
|
|
420
|
-
throw new PdfRasterException(
|
|
421
|
-
PdfErrorCode.OUT_OF_MEMORY,
|
|
422
|
-
"Could not allocate a " + pxW + "x" + pxH + " ARGB_8888 bitmap ("
|
|
423
|
-
+ ((long) pxW * pxH * 4L) + " bytes).",
|
|
424
|
-
e);
|
|
425
|
-
}
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Bitmap -> pixels, with the stride the platform actually used.
|
|
430
|
-
*
|
|
431
|
-
* TWO DESTINATIONS. With {@code slot == RasterPixels.NO_SLOT} this is the
|
|
432
|
-
* original: a {@code byte[]} on the Java heap, filled by
|
|
433
|
-
* {@code copyPixelsToBuffer}. With a real slot it tries native memory first
|
|
434
|
-
* and falls back to that same heap path on any refusal, so the heap path is
|
|
435
|
-
* never dead code and never diverges — it is what the base64 transport, every
|
|
436
|
-
* pixel-reading caller and the whole instrumented suite still use.
|
|
437
|
-
*
|
|
438
|
-
* Everything below about stride, premultiplication and byte order is true of
|
|
439
|
-
* BOTH, because both copy the identical bytes out of the identical bitmap: the
|
|
440
|
-
* native path's memcpy from {@code AndroidBitmap_lockPixels} and Skia's memcpy
|
|
441
|
-
* inside {@code nativeCopyPixelsToBuffer} read the same
|
|
442
|
-
* {@code bitmap.getPixels()} address for the same {@code getByteCount()}
|
|
443
|
-
* length. (Byte ORDER never enters it — {@code copyPixelsToBuffer} into a
|
|
444
|
-
* {@code ByteBuffer} is a raw memcpy and ignores the buffer's
|
|
445
|
-
* {@code order()}. That would NOT be true of an {@code IntBuffer}.)
|
|
446
|
-
*
|
|
447
|
-
* {@code copyPixelsToBuffer} is a raw copy of the bitmap's pixel memory:
|
|
448
|
-
* {@code getByteCount()} is {@code getRowBytes() * getHeight()}, so any row
|
|
449
|
-
* padding is copied through and {@code getRowBytes()} is the honest
|
|
450
|
-
* {@code rowBytes}. Assuming {@code width * 4} here would not fail — it would
|
|
451
|
-
* SHEAR the image by one pixel per row on any padded allocation, which reads
|
|
452
|
-
* as a plausible skew rather than an error.
|
|
453
|
-
*
|
|
454
|
-
* On the three levels measured that shear is unreachable, and the comment used
|
|
455
|
-
* to imply otherwise: {@code Bitmap.createBitmap} allocates at
|
|
456
|
-
* {@code SkImageInfo::minRowBytes()}, i.e. exactly {@code width * 4}, so the
|
|
457
|
-
* assumption and the reading agree at every width tested.
|
|
458
|
-
* {@code rowBytesIsReportedNotAssumed} pins the reported value against the
|
|
459
|
-
* platform's own stride and says so in its own comment; the padded path is
|
|
460
|
-
* exercised in vitest against the fake backend. Reading the value is still the
|
|
461
|
-
* right call — it costs nothing and it is correct on a device that pads.
|
|
462
|
-
*
|
|
463
|
-
* The bytes are PREMULTIPLIED. Bitmaps are premultiplied by default and this
|
|
464
|
-
* code never calls {@code setPremultiplied(false)} — doing so would make a
|
|
465
|
-
* {@code background: 'transparent'} raster composite wrongly rather than
|
|
466
|
-
* visibly fail.
|
|
467
|
-
*
|
|
468
|
-
* The TS side DECLARES that rather than assuming it: {@code ANDROID_ALPHA} in
|
|
469
|
-
* {@code src/rasterizer/android.ts} sets {@code RasterPixels.alpha} to
|
|
470
|
-
* {@code 'premultiplied'} and {@code ingest.ts} maps it to
|
|
471
|
-
* {@code AlphaType.Premul}. It is a constant there, not a field of the wire
|
|
472
|
-
* result, because it is a property of {@code Bitmap} and cannot vary per
|
|
473
|
-
* render. It is NOT a global default — PDFium and pdf.js both emit STRAIGHT
|
|
474
|
-
* alpha, so the other backends declare the opposite.
|
|
475
|
-
*/
|
|
476
|
-
private static RasterPixels extract(Bitmap bitmap, int pxW, int pxH, long slot)
|
|
477
|
-
throws PdfRasterException {
|
|
478
|
-
int rowBytes = bitmap.getRowBytes();
|
|
479
|
-
int byteCount = bitmap.getByteCount();
|
|
480
|
-
// FIRST, and before either destination is allocated. Both paths copy exactly
|
|
481
|
-
// byteCount bytes and both would shear on a bad stride, so this cannot move
|
|
482
|
-
// inside one of the branches.
|
|
483
|
-
if (rowBytes < pxW * 4 || byteCount < rowBytes * pxH) {
|
|
484
|
-
throw new PdfRasterException(
|
|
485
|
-
PdfErrorCode.BACKEND_FAILURE,
|
|
486
|
-
"Bitmap reported an impossible layout: " + pxW + "x" + pxH
|
|
487
|
-
+ " rowBytes=" + rowBytes + " byteCount=" + byteCount + ".");
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
// THE ONE-COPY PATH. Tried first when a slot was asked for, and every
|
|
491
|
-
// failure mode of it is a `false` that falls through to the heap path below
|
|
492
|
-
// — including running out of native memory, which is why the OUT_OF_MEMORY
|
|
493
|
-
// mapping still comes from the Java allocation and not from here.
|
|
494
|
-
//
|
|
495
|
-
// `isLoaded()` is checked rather than assumed: every method on the bridge is
|
|
496
|
-
// `native`, so on a build with no libpdfcanvasjsi.so this call is an
|
|
497
|
-
// UnsatisfiedLinkError, not a false.
|
|
498
|
-
if (slot != RasterPixels.NO_SLOT && PdfCanvasPixelBridge.isLoaded()) {
|
|
499
|
-
boolean stored;
|
|
500
|
-
try {
|
|
501
|
-
stored =
|
|
502
|
-
PdfCanvasPixelBridge.nativeStoreFromBitmap(slot, bitmap, pxW, pxH, byteCount);
|
|
503
|
-
} catch (Throwable t) {
|
|
504
|
-
// The JNI method reports every failure of its OWN as a false. What can
|
|
505
|
-
// still arrive as a throwable is a Java exception raised by the platform
|
|
506
|
-
// calls it makes on the way — {@code AndroidBitmap_getInfo} and
|
|
507
|
-
// {@code AndroidBitmap_lockPixels} may leave one pending, and a JNI
|
|
508
|
-
// method that returns while an exception is pending throws it at the
|
|
509
|
-
// caller regardless of the value it returned. Either way the answer is
|
|
510
|
-
// the same and it is the point of this catch: fall through to the heap.
|
|
511
|
-
// Letting it escape would abandon a raster that IS already drawn in
|
|
512
|
-
// exchange for nothing.
|
|
513
|
-
stored = false;
|
|
514
|
-
}
|
|
515
|
-
if (stored) {
|
|
516
|
-
return RasterPixels.inNativeSlot(
|
|
517
|
-
slot, pxW, pxH, rowBytes, RasterPixels.FORMAT_RGBA_8888);
|
|
518
|
-
}
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
byte[] out;
|
|
522
|
-
try {
|
|
523
|
-
out = new byte[byteCount];
|
|
524
|
-
} catch (OutOfMemoryError e) {
|
|
525
|
-
throw new PdfRasterException(
|
|
526
|
-
PdfErrorCode.OUT_OF_MEMORY,
|
|
527
|
-
"Could not allocate " + byteCount + " bytes for raster pixels.", e);
|
|
528
|
-
}
|
|
529
|
-
// ByteBuffer.wrap starts at position 0, and copyPixelsToBuffer ADVANCES the
|
|
530
|
-
// position by byteCount as it copies. Nothing downstream reads the buffer —
|
|
531
|
-
// `out` is indexed from 0 — and a fresh wrapper is made every call, so no
|
|
532
|
-
// rewind is needed and none is implied. A REUSED buffer would need one.
|
|
533
|
-
bitmap.copyPixelsToBuffer(ByteBuffer.wrap(out));
|
|
534
|
-
return new RasterPixels(out, pxW, pxH, rowBytes, RasterPixels.FORMAT_RGBA_8888);
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
/* ---------------------------------------------------------------- *
|
|
538
|
-
* Teardown
|
|
539
|
-
* ---------------------------------------------------------------- */
|
|
540
|
-
|
|
541
|
-
private void requireOpen() throws PdfRasterException {
|
|
542
|
-
if (closed || pool.isClosed()) {
|
|
543
|
-
throw new PdfRasterException(
|
|
544
|
-
PdfErrorCode.BACKEND_FAILURE, "Document handle is closed.");
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
@Override
|
|
549
|
-
public void close() {
|
|
550
|
-
if (closed) {
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
closed = true;
|
|
554
|
-
pool.close();
|
|
555
|
-
if (spooled != null) {
|
|
556
|
-
//noinspection ResultOfMethodCallIgnored
|
|
557
|
-
spooled.delete();
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
public boolean isClosed() {
|
|
562
|
-
return closed;
|
|
563
|
-
}
|
|
564
|
-
}
|