@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,277 +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
|
-
import android.os.Build;
|
|
8
|
-
import android.os.ParcelFileDescriptor;
|
|
9
|
-
|
|
10
|
-
import java.io.File;
|
|
11
|
-
import java.io.FileOutputStream;
|
|
12
|
-
import java.nio.charset.Charset;
|
|
13
|
-
import java.util.concurrent.atomic.AtomicBoolean;
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Where this backend keeps what it has MEASURED about the platform it is running
|
|
17
|
-
* on, as opposed to what the documentation promises.
|
|
18
|
-
*
|
|
19
|
-
* Two quirks live here, both found on the API 24 floor and both invisible on API
|
|
20
|
-
* 31 and 33. They are kept together because they share a shape: the platform
|
|
21
|
-
* does not report either of them, so the only way to know is to look.
|
|
22
|
-
*/
|
|
23
|
-
public final class PdfPlatformQuirks {
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* The lowest API level MEASURED to survive a failed {@code PdfRenderer}
|
|
27
|
-
* construction. 24 does not; 31 and 33 do.
|
|
28
|
-
*
|
|
29
|
-
* The bound is deliberately the lowest level measured GOOD rather than one
|
|
30
|
-
* above the level measured bad: 25 through 30 were never tested (no image
|
|
31
|
-
* available in this session), and treating untested territory as safe is how a
|
|
32
|
-
* process-killing bug ships. The cost of being wrong in this direction is that
|
|
33
|
-
* a device between 25 and 30 refuses further documents after it has already
|
|
34
|
-
* met a corrupt one; the cost of being wrong in the other direction is a
|
|
35
|
-
* SIGSEGV.
|
|
36
|
-
*/
|
|
37
|
-
static final int LOWEST_SDK_MEASURED_SAFE_AFTER_FAILED_OPEN = 31;
|
|
38
|
-
|
|
39
|
-
private static final AtomicBoolean pdfiumPoisoned = new AtomicBoolean(false);
|
|
40
|
-
|
|
41
|
-
private PdfPlatformQuirks() {}
|
|
42
|
-
|
|
43
|
-
/* ================================================================ *
|
|
44
|
-
* Quirk 1 — a failed PdfRenderer construction poisons PDFium
|
|
45
|
-
* ================================================================ */
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* True when a throwing {@code PdfRenderer} constructor is expected to leave
|
|
49
|
-
* this process's PDFium unusable.
|
|
50
|
-
*
|
|
51
|
-
* See {@link PdfPreflight} for the mechanism and the measurement. Preflight
|
|
52
|
-
* removes the two common triggers before the platform class ever sees the
|
|
53
|
-
* file; this latch covers what is left — a file that looks like a PDF and is
|
|
54
|
-
* structurally broken deeper in.
|
|
55
|
-
*/
|
|
56
|
-
static boolean failedNativeOpenIsPoisonous() {
|
|
57
|
-
return Build.VERSION.SDK_INT < LOWEST_SDK_MEASURED_SAFE_AFTER_FAILED_OPEN;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
static void recordFailedNativeOpen() {
|
|
61
|
-
if (failedNativeOpenIsPoisonous()) {
|
|
62
|
-
pdfiumPoisoned.set(true);
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
static boolean isPdfiumPoisoned() {
|
|
67
|
-
return pdfiumPoisoned.get();
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Public form of {@link #isPdfiumPoisoned()}, for the React binding, which
|
|
72
|
-
* lives in a different package and has no business with the rest of this
|
|
73
|
-
* class's internals. A host can surface "PDF viewing needs an app restart"
|
|
74
|
-
* instead of watching every subsequent open fail.
|
|
75
|
-
*/
|
|
76
|
-
public static boolean isBackendPoisoned() {
|
|
77
|
-
return pdfiumPoisoned.get();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Refuses rather than risking the segfault.
|
|
82
|
-
*
|
|
83
|
-
* This is a session-wide capitulation and it is meant to be: once a
|
|
84
|
-
* construction has thrown on an affected build, the reference count is already
|
|
85
|
-
* wrong and there is no way to put it back. An error every consumer can show
|
|
86
|
-
* is strictly better than a crash on whichever document happens to be opened
|
|
87
|
-
* next, which will not be the one that caused it.
|
|
88
|
-
*/
|
|
89
|
-
static void requireUsablePdfium() throws PdfRasterException {
|
|
90
|
-
if (pdfiumPoisoned.get()) {
|
|
91
|
-
throw new PdfRasterException(
|
|
92
|
-
PdfErrorCode.BACKEND_FAILURE,
|
|
93
|
-
"Refusing to open a document: a previous file failed to load and, on API "
|
|
94
|
-
+ Build.VERSION.SDK_INT + ", that leaves the platform's PDFium library "
|
|
95
|
-
+ "reference count corrupted — the next successful load would segfault. "
|
|
96
|
-
+ "Restarting the app clears it.");
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
/** Test hook. Package-private, and the only way this flag is ever cleared. */
|
|
101
|
-
static void setPdfiumPoisonedForTesting(boolean poisoned) {
|
|
102
|
-
pdfiumPoisoned.set(poisoned);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/* ================================================================ *
|
|
106
|
-
* Quirk 2 — /Rotate is not applied when rendering
|
|
107
|
-
* ================================================================ */
|
|
108
|
-
|
|
109
|
-
public enum RotationSupport {
|
|
110
|
-
/** A page carrying /Rotate renders upright, as the contract requires. */
|
|
111
|
-
SUPPORTED,
|
|
112
|
-
/**
|
|
113
|
-
* {@code getWidth()}/{@code getHeight()} swap for a /Rotate 90 page, but the
|
|
114
|
-
* CONTENT is drawn unrotated, and the y-flip uses the post-rotation height —
|
|
115
|
-
* so the page lands shifted and clipped. Measured on API 24:
|
|
116
|
-
*
|
|
117
|
-
* <pre>
|
|
118
|
-
* rotate=0 geometry=612x792 drawn=[0,0..611,791] red=[0,0..111,99]
|
|
119
|
-
* rotate=90 geometry=792x612 drawn=[0,0..611,611] red=ABSENT blue=[0,512..111,611]
|
|
120
|
-
* rotate=180 geometry=612x792 drawn=[0,0..611,791] red=[0,0..111,99]
|
|
121
|
-
* rotate=270 geometry=792x612 drawn=[0,0..611,611] red=ABSENT blue=[0,512..111,611]
|
|
122
|
-
* </pre>
|
|
123
|
-
*
|
|
124
|
-
* /Rotate 180 producing output identical to /Rotate 0 is the tell: the
|
|
125
|
-
* rotation is not applied at ANY angle. API 31 and 33 render all four
|
|
126
|
-
* correctly.
|
|
127
|
-
*/
|
|
128
|
-
UNSUPPORTED,
|
|
129
|
-
/** The probe could not run. Treated as unsupported by callers that care. */
|
|
130
|
-
UNKNOWN
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
private static volatile RotationSupport rotationSupport;
|
|
134
|
-
|
|
135
|
-
/**
|
|
136
|
-
* Renders a tiny purpose-built /Rotate 90 page and looks at where the ink
|
|
137
|
-
* landed. Computed once per process and cached.
|
|
138
|
-
*
|
|
139
|
-
* Probed rather than keyed off {@code Build.VERSION.SDK_INT} for the same
|
|
140
|
-
* reason the latch above is conservative: 25 to 30 are untested, and a version
|
|
141
|
-
* check would have to guess about them. A probe answers for the device it is
|
|
142
|
-
* actually running on. It costs one ~450-byte file and one 200x100 render.
|
|
143
|
-
*
|
|
144
|
-
* @param scratchDir somewhere writable; the probe file is deleted afterwards.
|
|
145
|
-
*/
|
|
146
|
-
public static RotationSupport rotationSupport(File scratchDir) {
|
|
147
|
-
RotationSupport cached = rotationSupport;
|
|
148
|
-
if (cached != null) {
|
|
149
|
-
return cached;
|
|
150
|
-
}
|
|
151
|
-
synchronized (PdfPlatformQuirks.class) {
|
|
152
|
-
if (rotationSupport == null) {
|
|
153
|
-
rotationSupport = probeRotation(scratchDir);
|
|
154
|
-
}
|
|
155
|
-
return rotationSupport;
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
private static final Charset LATIN1 = Charset.forName("ISO-8859-1");
|
|
160
|
-
|
|
161
|
-
/** 100x200pt, /Rotate 90, with a red 50x50 marker at the UNROTATED top-left. */
|
|
162
|
-
private static byte[] rotationProbeDocument() {
|
|
163
|
-
// PDF user space is y-up from the bottom-left, so the unrotated top-left
|
|
164
|
-
// 50x50 of a 100x200 page is the rect (0, 150) to (50, 200).
|
|
165
|
-
String content = "1 1 1 rg 0 0 100 200 re f\n1 0 0 rg 0 150 50 50 re f\n";
|
|
166
|
-
StringBuilder body = new StringBuilder();
|
|
167
|
-
int[] offsets = new int[4];
|
|
168
|
-
StringBuilder pdf = new StringBuilder("%PDF-1.4\n");
|
|
169
|
-
|
|
170
|
-
offsets[0] = pdf.length();
|
|
171
|
-
pdf.append("1 0 obj\n<< /Length ").append(content.length()).append(" >>\nstream\n")
|
|
172
|
-
.append(content).append("endstream\nendobj\n");
|
|
173
|
-
offsets[1] = pdf.length();
|
|
174
|
-
pdf.append("2 0 obj\n<< /Type /Page /Parent 3 0 R /MediaBox [0 0 100 200] /Rotate 90 "
|
|
175
|
-
+ "/Contents 1 0 R /Resources << >> >>\nendobj\n");
|
|
176
|
-
offsets[2] = pdf.length();
|
|
177
|
-
pdf.append("3 0 obj\n<< /Type /Pages /Kids [2 0 R] /Count 1 >>\nendobj\n");
|
|
178
|
-
offsets[3] = pdf.length();
|
|
179
|
-
pdf.append("4 0 obj\n<< /Type /Catalog /Pages 3 0 R >>\nendobj\n");
|
|
180
|
-
|
|
181
|
-
int xref = pdf.length();
|
|
182
|
-
pdf.append("xref\n0 5\n0000000000 65535 f \n");
|
|
183
|
-
for (int offset : offsets) {
|
|
184
|
-
pdf.append(String.format("%010d 00000 n \n", offset));
|
|
185
|
-
}
|
|
186
|
-
pdf.append("trailer\n<< /Size 5 /Root 4 0 R >>\nstartxref\n").append(xref).append("\n%%EOF\n");
|
|
187
|
-
// Every byte written is ASCII, so String.length() above is the byte length.
|
|
188
|
-
return pdf.toString().getBytes(LATIN1);
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
private static RotationSupport probeRotation(File scratchDir) {
|
|
192
|
-
// The probe is a PdfRenderer construction like any other, so it is subject
|
|
193
|
-
// to quirk 1 in both directions: it must not run on an already-poisoned
|
|
194
|
-
// process (the constructor would be the segfault), and if it throws it must
|
|
195
|
-
// arm the latch like every other construction site does. Neither was true
|
|
196
|
-
// before — this was the one place that built a PdfRenderer inside a bare
|
|
197
|
-
// `catch (Throwable) { return UNKNOWN; }`, so a throw here was swallowed and
|
|
198
|
-
// the next open walked into it.
|
|
199
|
-
if (pdfiumPoisoned.get()) {
|
|
200
|
-
return RotationSupport.UNKNOWN;
|
|
201
|
-
}
|
|
202
|
-
File probe = new File(scratchDir, "pdfcanvas-rotation-probe.pdf");
|
|
203
|
-
ParcelFileDescriptor pfd = null;
|
|
204
|
-
PdfRenderer renderer = null;
|
|
205
|
-
PdfRenderer.Page page = null;
|
|
206
|
-
Bitmap bitmap = null;
|
|
207
|
-
try {
|
|
208
|
-
if (!scratchDir.exists() && !scratchDir.mkdirs()) {
|
|
209
|
-
return RotationSupport.UNKNOWN;
|
|
210
|
-
}
|
|
211
|
-
try (FileOutputStream out = new FileOutputStream(probe)) {
|
|
212
|
-
out.write(rotationProbeDocument());
|
|
213
|
-
}
|
|
214
|
-
pfd = ParcelFileDescriptor.open(probe, ParcelFileDescriptor.MODE_READ_ONLY);
|
|
215
|
-
try {
|
|
216
|
-
renderer = new PdfRenderer(pfd);
|
|
217
|
-
} catch (Throwable t) {
|
|
218
|
-
// The probe document is authored by this class and is valid, so this
|
|
219
|
-
// should be unreachable — but "should be" is exactly the assumption that
|
|
220
|
-
// costs a process. A constructor that threw has already left the
|
|
221
|
-
// half-built finalizable object behind, so the latch is armed here for
|
|
222
|
-
// the same reason PdfRendererPool arms it.
|
|
223
|
-
recordFailedNativeOpen();
|
|
224
|
-
throw t;
|
|
225
|
-
}
|
|
226
|
-
pfd = null; // ownership transferred
|
|
227
|
-
page = renderer.openPage(0);
|
|
228
|
-
|
|
229
|
-
// Past the constructor now. A throw from here on is not a poisoning event
|
|
230
|
-
// — the document is open and PDFium's reference count is balanced — so
|
|
231
|
-
// UNKNOWN is the honest answer and the catch below produces it.
|
|
232
|
-
if (page.getWidth() != 200 || page.getHeight() != 100) {
|
|
233
|
-
// Dimensions are not even swapped: /Rotate is ignored wholesale.
|
|
234
|
-
return RotationSupport.UNSUPPORTED;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
bitmap = Bitmap.createBitmap(200, 100, Bitmap.Config.ARGB_8888);
|
|
238
|
-
bitmap.eraseColor(Color.WHITE);
|
|
239
|
-
page.render(bitmap, null, new Matrix(), PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY);
|
|
240
|
-
|
|
241
|
-
// A clockwise quarter turn — which is what /Rotate means — puts the
|
|
242
|
-
// unrotated top-left marker at the displayed top-RIGHT.
|
|
243
|
-
int sample = bitmap.getPixel(185, 15);
|
|
244
|
-
boolean red = Color.red(sample) > 200 && Color.green(sample) < 60 && Color.blue(sample) < 60;
|
|
245
|
-
return red ? RotationSupport.SUPPORTED : RotationSupport.UNSUPPORTED;
|
|
246
|
-
} catch (Throwable t) {
|
|
247
|
-
return RotationSupport.UNKNOWN;
|
|
248
|
-
} finally {
|
|
249
|
-
if (bitmap != null) {
|
|
250
|
-
bitmap.recycle();
|
|
251
|
-
}
|
|
252
|
-
if (page != null) {
|
|
253
|
-
page.close();
|
|
254
|
-
}
|
|
255
|
-
if (renderer != null) {
|
|
256
|
-
renderer.close();
|
|
257
|
-
}
|
|
258
|
-
if (pfd != null) {
|
|
259
|
-
try {
|
|
260
|
-
pfd.close();
|
|
261
|
-
} catch (Exception ignored) {
|
|
262
|
-
// Only reachable if the PdfRenderer constructor threw, and the
|
|
263
|
-
// throwing path already carries the real cause.
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
//noinspection ResultOfMethodCallIgnored
|
|
267
|
-
probe.delete();
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/** Test hook: forget the cached verdict so the probe can be re-run. */
|
|
272
|
-
static void resetRotationSupportForTesting() {
|
|
273
|
-
synchronized (PdfPlatformQuirks.class) {
|
|
274
|
-
rotationSupport = null;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
}
|
|
@@ -1,214 +0,0 @@
|
|
|
1
|
-
package tools.reekon.pdfcanvas;
|
|
2
|
-
|
|
3
|
-
import java.io.File;
|
|
4
|
-
import java.io.IOException;
|
|
5
|
-
import java.io.RandomAccessFile;
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Cheap structural checks run BEFORE {@code new PdfRenderer(...)} is allowed
|
|
9
|
-
* anywhere near the file.
|
|
10
|
-
*
|
|
11
|
-
* <h3>Why this exists, measured on device</h3>
|
|
12
|
-
*
|
|
13
|
-
* On API 24, a {@code PdfRenderer} CONSTRUCTOR THAT THROWS permanently poisons
|
|
14
|
-
* PDFium for the whole process. The constructor assigns its
|
|
15
|
-
* {@code ParcelFileDescriptor} field before calling {@code nativeCreate}, so a
|
|
16
|
-
* throw leaves a half-built, unreachable, FINALIZABLE object; when the collector
|
|
17
|
-
* runs it, {@code finalize -> doClose -> nativeClose(0)} decrements PDFium's
|
|
18
|
-
* library reference count with no matching initialise. The count goes below
|
|
19
|
-
* zero, {@code initializeLibraryIfNeeded} then never re-initialises, and the
|
|
20
|
-
* NEXT document load dereferences a null {@code CPDF_ModuleMgr} and takes the
|
|
21
|
-
* process down.
|
|
22
|
-
*
|
|
23
|
-
* Measured, not inferred — one failed construction plus a forced
|
|
24
|
-
* {@code System.gc(); System.runFinalization()} is enough:
|
|
25
|
-
*
|
|
26
|
-
* <pre>
|
|
27
|
-
* API 24: baseline good = OK(1)
|
|
28
|
-
* failure #1 -> IOException; collecting
|
|
29
|
-
* about to open good after 1 collected failures
|
|
30
|
-
* F libc: Fatal signal 11 (SIGSEGV), fault addr 0x10
|
|
31
|
-
* #00 libpdfium.so (CPDF_Document::CPDF_Document(CPDF_Parser*)+96)
|
|
32
|
-
* API 31: SURVIVED 12
|
|
33
|
-
* API 33: SURVIVED 12
|
|
34
|
-
* </pre>
|
|
35
|
-
*
|
|
36
|
-
* So on the minSdk floor a single wrong file — a JPEG the user picked by
|
|
37
|
-
* mistake, a password-protected drawing — is not an error, it is a delayed
|
|
38
|
-
* process death. The two overwhelmingly common causes are cheap to detect from
|
|
39
|
-
* the bytes, and detecting them here means the platform class never sees the
|
|
40
|
-
* file at all.
|
|
41
|
-
*
|
|
42
|
-
* <h3>What this is NOT</h3>
|
|
43
|
-
*
|
|
44
|
-
* It is not a PDF validator and must not grow into one. It answers two
|
|
45
|
-
* questions — "does this even claim to be a PDF" and "is it encrypted" — and
|
|
46
|
-
* anything subtler (a broken xref, a truncated object stream) still reaches
|
|
47
|
-
* PdfRenderer and is still caught by {@link PdfPlatformQuirks}'s latch. Being
|
|
48
|
-
* incomplete is fine; being WRONG is not, which is why the encryption test
|
|
49
|
-
* insists on the full indirect-reference shape rather than a bare substring.
|
|
50
|
-
*/
|
|
51
|
-
final class PdfPreflight {
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* PDFium scans this far for the header rather than insisting on byte 0, and a
|
|
55
|
-
* good many real-world PDFs have junk in front of {@code %PDF-}. Matching its
|
|
56
|
-
* tolerance means preflight rejects only files PdfRenderer would also reject.
|
|
57
|
-
*/
|
|
58
|
-
private static final int HEADER_SEARCH_BYTES = 1024;
|
|
59
|
-
|
|
60
|
-
/** Enough to cover a linearised file's first-page xref and trailer. */
|
|
61
|
-
private static final int HEAD_SCAN_BYTES = 32 * 1024;
|
|
62
|
-
/** Enough to cover the trailer, the xref, and a few incremental updates. */
|
|
63
|
-
private static final int TAIL_SCAN_BYTES = 256 * 1024;
|
|
64
|
-
|
|
65
|
-
private static final byte[] PDF_HEADER = {'%', 'P', 'D', 'F', '-'};
|
|
66
|
-
private static final byte[] ENCRYPT = {'/', 'E', 'n', 'c', 'r', 'y', 'p', 't'};
|
|
67
|
-
|
|
68
|
-
private PdfPreflight() {}
|
|
69
|
-
|
|
70
|
-
static void check(File file) throws PdfRasterException {
|
|
71
|
-
if (!file.exists()) {
|
|
72
|
-
throw new PdfRasterException(
|
|
73
|
-
PdfErrorCode.NOT_FOUND, "No file at " + file.getAbsolutePath() + ".");
|
|
74
|
-
}
|
|
75
|
-
if (!file.isFile() || !file.canRead()) {
|
|
76
|
-
throw new PdfRasterException(
|
|
77
|
-
PdfErrorCode.NOT_FOUND, "Not a readable file: " + file.getAbsolutePath() + ".");
|
|
78
|
-
}
|
|
79
|
-
long length = file.length();
|
|
80
|
-
if (length == 0L) {
|
|
81
|
-
// Reported as corrupt rather than not-found: the path resolved, so the
|
|
82
|
-
// actionable truth is that the download or copy never finished.
|
|
83
|
-
throw new PdfRasterException(
|
|
84
|
-
PdfErrorCode.CORRUPT, "File is empty: " + file.getAbsolutePath() + ".");
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
byte[] head;
|
|
88
|
-
byte[] tail;
|
|
89
|
-
try (RandomAccessFile raf = new RandomAccessFile(file, "r")) {
|
|
90
|
-
head = read(raf, 0L, (int) Math.min(length, HEAD_SCAN_BYTES));
|
|
91
|
-
long tailStart = Math.max(0L, length - TAIL_SCAN_BYTES);
|
|
92
|
-
// The ONLY case where the tail can be skipped is a file the head already
|
|
93
|
-
// covers END TO END. Comparing tailStart against head.length instead is
|
|
94
|
-
// the bug this line used to have: for any length in
|
|
95
|
-
// (HEAD_SCAN_BYTES, TAIL_SCAN_BYTES] the tail starts at 0, which is less
|
|
96
|
-
// than head.length, so the tail was dropped while the head reached only
|
|
97
|
-
// 32 KB — and every byte from there to EOF, TRAILER INCLUDED, went
|
|
98
|
-
// unread. A 60 KB encrypted drawing is an entirely ordinary document, and
|
|
99
|
-
// on API 24 it then reached `new PdfRenderer(...)` and poisoned PDFium
|
|
100
|
-
// for the session: exactly the outcome this class exists to prevent.
|
|
101
|
-
// preflightFindsTheEncryptTrailerBeyondTheHeadWindow pins it.
|
|
102
|
-
tail = length <= head.length ? new byte[0] : read(raf, tailStart, TAIL_SCAN_BYTES);
|
|
103
|
-
} catch (IOException e) {
|
|
104
|
-
throw new PdfRasterException(
|
|
105
|
-
PdfErrorCode.CORRUPT, "Could not read " + file.getName() + ".", e);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
if (indexOf(head, PDF_HEADER, 0, Math.min(head.length, HEADER_SEARCH_BYTES)) < 0) {
|
|
109
|
-
throw new PdfRasterException(
|
|
110
|
-
PdfErrorCode.CORRUPT,
|
|
111
|
-
"Not a PDF: no %PDF- header in the first " + HEADER_SEARCH_BYTES
|
|
112
|
-
+ " bytes of " + file.getName() + ".");
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
if (hasEncryptReference(head) || hasEncryptReference(tail)) {
|
|
116
|
-
throw new PdfRasterException(
|
|
117
|
-
PdfErrorCode.PASSWORD_REQUIRED,
|
|
118
|
-
"The document is encrypted: " + file.getName() + ".");
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
private static byte[] read(RandomAccessFile raf, long offset, int max) throws IOException {
|
|
123
|
-
raf.seek(offset);
|
|
124
|
-
byte[] buffer = new byte[max];
|
|
125
|
-
int read = 0;
|
|
126
|
-
while (read < max) {
|
|
127
|
-
int n = raf.read(buffer, read, max - read);
|
|
128
|
-
if (n < 0) {
|
|
129
|
-
break;
|
|
130
|
-
}
|
|
131
|
-
read += n;
|
|
132
|
-
}
|
|
133
|
-
if (read == max) {
|
|
134
|
-
return buffer;
|
|
135
|
-
}
|
|
136
|
-
byte[] exact = new byte[read];
|
|
137
|
-
System.arraycopy(buffer, 0, exact, 0, read);
|
|
138
|
-
return exact;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* True when the buffer contains {@code /Encrypt <num> <num> R}.
|
|
143
|
-
*
|
|
144
|
-
* The full shape is required, not just the name. A bare {@code /Encrypt}
|
|
145
|
-
* substring can appear inside an uncompressed content stream or a piece of
|
|
146
|
-
* metadata, and refusing such a document would be a false "this file needs a
|
|
147
|
-
* password" — the single most confusing thing this class could say. The spec
|
|
148
|
-
* requires the trailer's /Encrypt value to be an indirect reference, so
|
|
149
|
-
* demanding the reference costs nothing in recall and removes almost all of
|
|
150
|
-
* the false-positive surface.
|
|
151
|
-
*/
|
|
152
|
-
private static boolean hasEncryptReference(byte[] buffer) {
|
|
153
|
-
int from = 0;
|
|
154
|
-
while (true) {
|
|
155
|
-
int at = indexOf(buffer, ENCRYPT, from, buffer.length);
|
|
156
|
-
if (at < 0) {
|
|
157
|
-
return false;
|
|
158
|
-
}
|
|
159
|
-
if (isIndirectReferenceAt(buffer, at + ENCRYPT.length)) {
|
|
160
|
-
return true;
|
|
161
|
-
}
|
|
162
|
-
from = at + 1;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
private static boolean isIndirectReferenceAt(byte[] buffer, int start) {
|
|
167
|
-
int i = skipWhitespace(buffer, start);
|
|
168
|
-
int digits = countDigits(buffer, i);
|
|
169
|
-
if (digits == 0) {
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
i = skipWhitespace(buffer, i + digits);
|
|
173
|
-
digits = countDigits(buffer, i);
|
|
174
|
-
if (digits == 0) {
|
|
175
|
-
return false;
|
|
176
|
-
}
|
|
177
|
-
i = skipWhitespace(buffer, i + digits);
|
|
178
|
-
return i < buffer.length && buffer[i] == 'R';
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
private static int skipWhitespace(byte[] buffer, int from) {
|
|
182
|
-
int i = from;
|
|
183
|
-
while (i < buffer.length) {
|
|
184
|
-
byte b = buffer[i];
|
|
185
|
-
if (b != ' ' && b != '\n' && b != '\r' && b != '\t' && b != 0 && b != 0x0c) {
|
|
186
|
-
break;
|
|
187
|
-
}
|
|
188
|
-
i++;
|
|
189
|
-
}
|
|
190
|
-
return i;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
private static int countDigits(byte[] buffer, int from) {
|
|
194
|
-
int i = from;
|
|
195
|
-
while (i < buffer.length && buffer[i] >= '0' && buffer[i] <= '9') {
|
|
196
|
-
i++;
|
|
197
|
-
}
|
|
198
|
-
return i - from;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
private static int indexOf(byte[] haystack, byte[] needle, int from, int limit) {
|
|
202
|
-
int end = Math.min(limit, haystack.length) - needle.length;
|
|
203
|
-
outer:
|
|
204
|
-
for (int i = Math.max(0, from); i <= end; i++) {
|
|
205
|
-
for (int j = 0; j < needle.length; j++) {
|
|
206
|
-
if (haystack[i + j] != needle[j]) {
|
|
207
|
-
continue outer;
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
return i;
|
|
211
|
-
}
|
|
212
|
-
return -1;
|
|
213
|
-
}
|
|
214
|
-
}
|