@reekon-tools/react-native-pdf-canvas 0.2.0 → 0.3.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/PdfCanvas.podspec +12 -4
- package/README.md +22 -0
- package/android/src/androidTest/java/tools/reekon/pdfcanvas/PdfCanvasNativeTest.java +34 -10
- package/android/src/main/cpp/pdfcanvas-jni.cpp +4 -2
- package/android/src/main/java/tools/reekon/pdfcanvas/PdfCanvasNative.java +5 -0
- package/android/src/reactnative/java/tools/reekon/pdfcanvas/rn/PdfCanvasModule.java +10 -3
- package/dist/controller.d.ts +23 -1
- package/dist/controller.js +28 -16
- package/dist/rasterizer/fake.d.ts +14 -2
- package/dist/rasterizer/fake.js +93 -36
- package/dist/rasterizer/native-bridge.d.ts +11 -0
- package/dist/rasterizer/native-bridge.js +1 -0
- package/dist/rasterizer/web/engine.js +26 -7
- package/dist/react/usePdfDocument.d.ts +11 -1
- package/dist/react/usePdfDocument.js +21 -12
- package/dist/react/usePdfLayer.d.ts +19 -2
- package/dist/react/usePdfLayer.js +51 -8
- package/dist/rotation.d.ts +59 -0
- package/dist/rotation.js +85 -0
- package/dist/types.d.ts +20 -0
- package/ios/Sources/PdfCanvasBridge/PdfCanvasModule.mm +5 -0
- package/native/CMakeLists.txt +7 -0
- package/native/core/include/pdfcanvas/types.h +8 -0
- package/native/core/src/document.cpp +25 -5
- package/native/tests/test_document.cpp +178 -0
- package/package.json +1 -1
- package/scripts/fetch-pdfium.mjs +75 -3
- package/scripts/pdfium-manifest.json +2 -0
|
@@ -329,6 +329,184 @@ TEST(openBytesMatchesOpenFile) {
|
|
|
329
329
|
CHECK_EQ(px::compare(a, b).differing, 0);
|
|
330
330
|
}
|
|
331
331
|
|
|
332
|
+
/* ------------------------------------------------------------------ *
|
|
333
|
+
* Host rotation — on top of the document's /Rotate
|
|
334
|
+
* ------------------------------------------------------------------ */
|
|
335
|
+
|
|
336
|
+
namespace {
|
|
337
|
+
|
|
338
|
+
RasterRequest turned(RasterRequest request, int rotation) {
|
|
339
|
+
request.rotation = rotation;
|
|
340
|
+
return request;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// `upright` turned `rotation` degrees clockwise, pixel for pixel — the
|
|
344
|
+
/// reference a rotated render is held to. Per case rather than
|
|
345
|
+
/// transpose-plus-flip, so the DIRECTION is stated and not derived: a core
|
|
346
|
+
/// turning pages anticlockwise would still be "a rotation".
|
|
347
|
+
RasterPixels turnClockwise(const RasterPixels& upright, int rotation) {
|
|
348
|
+
const bool swap = rotation != 180;
|
|
349
|
+
RasterPixels out;
|
|
350
|
+
out.width = swap ? upright.height : upright.width;
|
|
351
|
+
out.height = swap ? upright.width : upright.height;
|
|
352
|
+
out.rowBytes = out.width * 4;
|
|
353
|
+
out.bytes.assign(static_cast<size_t>(out.rowBytes) * static_cast<size_t>(out.height), 0);
|
|
354
|
+
for (int y = 0; y < out.height; y++) {
|
|
355
|
+
for (int x = 0; x < out.width; x++) {
|
|
356
|
+
int sx = 0;
|
|
357
|
+
int sy = 0;
|
|
358
|
+
if (rotation == 90) {
|
|
359
|
+
sx = y;
|
|
360
|
+
sy = upright.height - 1 - x;
|
|
361
|
+
} else if (rotation == 180) {
|
|
362
|
+
sx = upright.width - 1 - x;
|
|
363
|
+
sy = upright.height - 1 - y;
|
|
364
|
+
} else {
|
|
365
|
+
sx = upright.width - 1 - y;
|
|
366
|
+
sy = x;
|
|
367
|
+
}
|
|
368
|
+
const uint8_t* from = upright.bytes.data() + static_cast<size_t>(sy) * upright.rowBytes +
|
|
369
|
+
static_cast<size_t>(sx) * 4;
|
|
370
|
+
uint8_t* to = out.bytes.data() + static_cast<size_t>(y) * out.rowBytes + static_cast<size_t>(x) * 4;
|
|
371
|
+
std::copy(from, from + 4, to);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/// The registration page's pattern: bars 2.5pt wide on 13.3 / 11.1pt
|
|
378
|
+
/// intervals, so every edge sits on a fraction of a device pixel at every
|
|
379
|
+
/// scale and no rect can align with it by accident. Axis-aligned throughout.
|
|
380
|
+
std::string fractionalBars() {
|
|
381
|
+
std::string content = "1 0 0 rg 0 0 100 50 re f\n0 0 1 rg 100 50 100 50 re f\n0.2 0.2 0.25 rg\n";
|
|
382
|
+
for (double x = 3.7; x < 200; x += 13.3) content += std::to_string(x) + " 0 2.5 100 re f\n";
|
|
383
|
+
for (double y = 2.9; y < 100; y += 11.1) content += "0 " + std::to_string(y) + " 200 2.5 re f\n";
|
|
384
|
+
return content;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
} // namespace
|
|
388
|
+
|
|
389
|
+
/// CATCHES: the rotate argument ignored, applied anticlockwise, or applied
|
|
390
|
+
/// without swapping the page box PDFium is handed — which squashes the turned
|
|
391
|
+
/// page into the upright box. Quadrants, so every corner is a channel-order
|
|
392
|
+
/// test too. Doc (y-down): TL blue, TR black, BL red, BR green.
|
|
393
|
+
TEST(aHostRotationTurnsThePageClockwise) {
|
|
394
|
+
auto document = openFixture(fixtures::quadrants(200, 100), "host-rotate.pdf");
|
|
395
|
+
|
|
396
|
+
// The whole page, in the TURNED page's doc space: 100x200 at 90 and 270.
|
|
397
|
+
auto r90 = document->render(turned(px::request(0, 0, 0, 100, 200, 2), 90), nullptr, nullptr);
|
|
398
|
+
CHECK_EQ(r90.width, 200);
|
|
399
|
+
CHECK_EQ(r90.height, 400);
|
|
400
|
+
CHECK_PIXEL(r90, 20, 20, kRed); // BL -> TL
|
|
401
|
+
CHECK_PIXEL(r90, 180, 20, kBlue); // TL -> TR
|
|
402
|
+
CHECK_PIXEL(r90, 180, 380, kBlack); // TR -> BR
|
|
403
|
+
CHECK_PIXEL(r90, 20, 380, kGreen); // BR -> BL
|
|
404
|
+
|
|
405
|
+
auto r180 = document->render(turned(px::request(0, 0, 0, 200, 100, 2), 180), nullptr, nullptr);
|
|
406
|
+
CHECK_EQ(r180.width, 400);
|
|
407
|
+
CHECK_EQ(r180.height, 200);
|
|
408
|
+
CHECK_PIXEL(r180, 20, 20, kGreen); // BR -> TL
|
|
409
|
+
CHECK_PIXEL(r180, 380, 20, kRed); // BL -> TR
|
|
410
|
+
CHECK_PIXEL(r180, 380, 180, kBlue); // TL -> BR
|
|
411
|
+
CHECK_PIXEL(r180, 20, 180, kBlack); // TR -> BL
|
|
412
|
+
|
|
413
|
+
auto r270 = document->render(turned(px::request(0, 0, 0, 100, 200, 2), 270), nullptr, nullptr);
|
|
414
|
+
CHECK_EQ(r270.width, 200);
|
|
415
|
+
CHECK_EQ(r270.height, 400);
|
|
416
|
+
CHECK_PIXEL(r270, 20, 20, kBlack); // TR -> TL
|
|
417
|
+
CHECK_PIXEL(r270, 180, 20, kGreen); // BR -> TR
|
|
418
|
+
CHECK_PIXEL(r270, 180, 380, kRed); // BL -> BR
|
|
419
|
+
CHECK_PIXEL(r270, 20, 380, kBlue); // TL -> BL
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/// Pixel for pixel, not just the corners, on fractionally placed axis-aligned
|
|
423
|
+
/// ink: a quarter turn keeps every edge axis-aligned at a mirrored fractional
|
|
424
|
+
/// offset, whose coverage the rasterizer accumulates to the identical value —
|
|
425
|
+
/// so the turned render must EQUAL the upright one turned, with no tolerance.
|
|
426
|
+
/// (The web backend measures the same against the same PDFium build.)
|
|
427
|
+
TEST(aRotatedRasterIsTheUprightRasterTurned) {
|
|
428
|
+
auto document = openFixture(fixtures::page({0, 0, 200, 100}, nullptr, 0, fractionalBars()), "host-rotate-exact.pdf");
|
|
429
|
+
auto upright = px::renderWhole(*document, 0, 2);
|
|
430
|
+
for (int rotation : {90, 180, 270}) {
|
|
431
|
+
const bool swap = rotation != 180;
|
|
432
|
+
auto rotated = document->render(
|
|
433
|
+
turned(px::request(0, 0, 0, swap ? 100 : 200, swap ? 200 : 100, 2), rotation), nullptr, nullptr);
|
|
434
|
+
auto expected = turnClockwise(upright, rotation);
|
|
435
|
+
CHECK_EQ(rotated.width, expected.width);
|
|
436
|
+
CHECK_EQ(rotated.height, expected.height);
|
|
437
|
+
const auto diff = px::compare(rotated, expected);
|
|
438
|
+
CHECK_MSG(diff.differing == 0, std::to_string(rotation) + ": " + std::to_string(diff.differing) + "/" +
|
|
439
|
+
std::to_string(diff.total) + " pixels differ, maxDelta " +
|
|
440
|
+
std::to_string(diff.maxDelta));
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/// The registration property survives the turn: a tile of a rotated page is
|
|
445
|
+
/// that region of the rotated whole page. The detail layer only ever draws
|
|
446
|
+
/// tiles, so a rotation that held for whole-page renders alone would ship
|
|
447
|
+
/// soft, misregistered detail. Same `maxDelta <= 1` allowance as the upright
|
|
448
|
+
/// property, for the same diagonal-edge reason.
|
|
449
|
+
TEST(aRotatedTileRegistersAgainstTheRotatedPage) {
|
|
450
|
+
auto document = openFixture(fixtures::grid(200, 100), "host-rotate-tile.pdf");
|
|
451
|
+
const double scale = 4.0;
|
|
452
|
+
auto whole = document->render(turned(px::request(0, 0, 0, 100, 200, scale), 90), nullptr, nullptr);
|
|
453
|
+
auto tile = document->render(turned(px::request(0, 37, 23, 41, 64, scale), 90), nullptr, nullptr);
|
|
454
|
+
CHECK_EQ(tile.width, 164);
|
|
455
|
+
CHECK_EQ(tile.height, 256);
|
|
456
|
+
const auto diff = px::compare(tile, whole, static_cast<int>(37 * scale), static_cast<int>(23 * scale));
|
|
457
|
+
const std::string summary = std::to_string(diff.differing) + "/" + std::to_string(diff.total) +
|
|
458
|
+
" pixels differ, maxDelta " + std::to_string(diff.maxDelta);
|
|
459
|
+
CHECK_MSG(diff.maxDelta <= 1, summary);
|
|
460
|
+
CHECK_MSG(diff.differing * 100 < diff.total * 2, summary);
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
/// The host's rotation STACKS on the document's own /Rotate rather than
|
|
464
|
+
/// replacing it: a page saved with /Rotate 90 and shown turned another 270 is
|
|
465
|
+
/// upright, and draws exactly as the same content saved unrotated.
|
|
466
|
+
TEST(hostRotationStacksOnTheDocumentsRotate) {
|
|
467
|
+
auto saved = openFixture(fixtures::rotated(90), "host-stack-saved.pdf"); // PDFium reports 792x612
|
|
468
|
+
auto plain = openFixture(fixtures::rotated(0), "host-stack-plain.pdf"); // 612x792
|
|
469
|
+
CHECK_NEAR(saved->geometry(0).width, 792, 0.01);
|
|
470
|
+
// Turned 270 by the host, the saved page is 612x792 in doc space again.
|
|
471
|
+
auto unwound = saved->render(turned(px::request(0, 0, 0, 612, 792, 0.5), 270), nullptr, nullptr);
|
|
472
|
+
auto upright = px::renderWhole(*plain, 0, 0.5);
|
|
473
|
+
CHECK_EQ(unwound.width, 306);
|
|
474
|
+
CHECK_EQ(unwound.height, 396);
|
|
475
|
+
CHECK_EQ(px::compare(unwound, upright).differing, 0);
|
|
476
|
+
// And the markers sit where the unrotated fixture puts them.
|
|
477
|
+
CHECK_PIXEL(unwound, 20, 20, kRed);
|
|
478
|
+
CHECK_PIXEL(unwound, 20, 380, kBlue);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/// Form fields turn with the page: `FPDF_FFLDraw` takes the same rotate
|
|
482
|
+
/// argument and the same swapped box as the content pass, so a widget lands
|
|
483
|
+
/// where the turned content says and not where the upright page had it.
|
|
484
|
+
TEST(formFieldsTurnWithThePage) {
|
|
485
|
+
auto document = openFixture(fixtures::formFieldPage(), "host-rotate-form.pdf");
|
|
486
|
+
auto with = document->render(turned(px::request(0, 0, 0, 200, 200, 1, true), 90), nullptr, nullptr);
|
|
487
|
+
// The widget's centre, doc (100,125), turned a quarter clockwise on a
|
|
488
|
+
// 200x200 page: (200 - 125, 100) = (75, 100).
|
|
489
|
+
const px::Pixel inside = px::at(with, 75, 100);
|
|
490
|
+
CHECK_MSG(!inside.near(kWhite), "the turned widget must paint at (75,100); got " + inside.str() + "\n" + px::map(with));
|
|
491
|
+
// (150,100) came from upright (100,50), above the widget: plain page.
|
|
492
|
+
CHECK_PIXEL(with, 150, 100, kWhite);
|
|
493
|
+
// The content's red marker, upright at doc (10..40, 160..190), turns to
|
|
494
|
+
// (10..40, 10..40).
|
|
495
|
+
CHECK_PIXEL(with, 25, 25, kRed);
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
/// Refused, not folded: `45 / 90` is 0 in integer arithmetic, and a request
|
|
499
|
+
/// that asked for a turn must not quietly come back upright.
|
|
500
|
+
TEST(aRotationThatIsNotAQuarterTurnIsRefused) {
|
|
501
|
+
auto document = openFixture(fixtures::quadrants(40, 40), "host-rotate-bad.pdf");
|
|
502
|
+
for (int bad : {45, -90, 360, 1}) {
|
|
503
|
+
EXPECT_ERROR(document->render(turned(px::request(0, 0, 0, 40, 40, 1), bad), nullptr, nullptr),
|
|
504
|
+
ErrorCode::BackendFailure);
|
|
505
|
+
}
|
|
506
|
+
// And the page is still usable afterwards.
|
|
507
|
+
CHECK_EQ(px::renderWhole(*document, 0).width, 40);
|
|
508
|
+
}
|
|
509
|
+
|
|
332
510
|
/* ------------------------------------------------------------------ *
|
|
333
511
|
* Failures
|
|
334
512
|
* ------------------------------------------------------------------ */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reekon-tools/react-native-pdf-canvas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "PDF pages as Skia images, drawn at exact doc-space rects inside somebody else's transform. One PDFium engine on iOS, Android and web. Owns no canvas, no gestures, no viewport state.",
|
|
5
5
|
"author": "REEKON Tools",
|
|
6
6
|
"license": "Apache-2.0",
|
package/scripts/fetch-pdfium.mjs
CHANGED
|
@@ -283,13 +283,56 @@ async function android() {
|
|
|
283
283
|
* ios
|
|
284
284
|
* ------------------------------------------------------------------ */
|
|
285
285
|
|
|
286
|
+
/**
|
|
287
|
+
* The minimum iOS version a Mach-O was built for, read from its LC_BUILD_VERSION
|
|
288
|
+
* (`minos`) via `vtool`. A fat binary prints one block per architecture; every
|
|
289
|
+
* slice must agree or the number is meaningless, so disagreement is an error.
|
|
290
|
+
*
|
|
291
|
+
* THIS IS THE FLOOR, NOT A GUESS. App Store processing compares the
|
|
292
|
+
* `MinimumOSVersion` a framework's Info.plist declares against what its binary
|
|
293
|
+
* was actually built for and rejects the upload (ITMS-90208) when the plist
|
|
294
|
+
* claims a lower floor. The first PDFium release of this package hardcoded
|
|
295
|
+
* 13.0 — a number inherited from the CoreGraphics era and from the upstream
|
|
296
|
+
* xcframework script this mirrors — while the dylib said 17.0, and the very
|
|
297
|
+
* first TestFlight upload was rejected. The plist is now written from the
|
|
298
|
+
* binary, and the manifest's declared value is checked against it, so the two
|
|
299
|
+
* cannot drift apart again.
|
|
300
|
+
*/
|
|
301
|
+
function machOMinimumOS(binary) {
|
|
302
|
+
const text = execFileSync('xcrun', ['vtool', '-show-build', binary], {
|
|
303
|
+
encoding: 'utf8',
|
|
304
|
+
});
|
|
305
|
+
const found = new Set();
|
|
306
|
+
for (const line of text.split('\n')) {
|
|
307
|
+
const m = /^\s*minos\s+(\d+(?:\.\d+)*)\s*$/.exec(line);
|
|
308
|
+
if (m) found.add(m[1]);
|
|
309
|
+
}
|
|
310
|
+
if (found.size === 0) {
|
|
311
|
+
throw new Error(
|
|
312
|
+
`${binary} has no LC_BUILD_VERSION minos; cannot determine its iOS floor.`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
if (found.size > 1) {
|
|
316
|
+
throw new Error(
|
|
317
|
+
`${binary} slices disagree about their iOS floor: ${[...found].join(', ')}.`,
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
return [...found][0];
|
|
321
|
+
}
|
|
322
|
+
|
|
286
323
|
/**
|
|
287
324
|
* Wraps a bare dylib as an iOS `PDFium.framework` (shallow bundle) so it can be
|
|
288
325
|
* embedded and code-signed like any other framework. Mirrors what
|
|
289
326
|
* espresso3389/pdfium-xcframework's build.sh does, minus the macOS/Catalyst
|
|
290
327
|
* slices this package does not ship.
|
|
291
328
|
*/
|
|
292
|
-
function makeIosFramework(
|
|
329
|
+
function makeIosFramework(
|
|
330
|
+
dylib,
|
|
331
|
+
frameworkDir,
|
|
332
|
+
headersDir,
|
|
333
|
+
platform,
|
|
334
|
+
minimumOSVersion,
|
|
335
|
+
) {
|
|
293
336
|
resetDir(frameworkDir);
|
|
294
337
|
const binary = join(frameworkDir, 'PDFium');
|
|
295
338
|
cpSync(dylib, binary);
|
|
@@ -328,7 +371,7 @@ function makeIosFramework(dylib, frameworkDir, headersDir, platform) {
|
|
|
328
371
|
<string>${platform}</string>
|
|
329
372
|
</array>
|
|
330
373
|
<key>MinimumOSVersion</key>
|
|
331
|
-
<string
|
|
374
|
+
<string>${minimumOSVersion}</string>
|
|
332
375
|
</dict>
|
|
333
376
|
</plist>
|
|
334
377
|
`,
|
|
@@ -343,7 +386,18 @@ async function ios() {
|
|
|
343
386
|
);
|
|
344
387
|
}
|
|
345
388
|
const out = join(outputRoot, 'ios');
|
|
346
|
-
const
|
|
389
|
+
const declaredMinOS = manifest.iosMinimumOSVersion;
|
|
390
|
+
if (
|
|
391
|
+
typeof declaredMinOS !== 'string' ||
|
|
392
|
+
!/^\d+(\.\d+)*$/.test(declaredMinOS)
|
|
393
|
+
) {
|
|
394
|
+
throw new Error(
|
|
395
|
+
'pdfium-manifest.json must declare iosMinimumOSVersion (e.g. "17.0").',
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
// The floor is part of the stamp so an output assembled by an older script
|
|
399
|
+
// (which wrote a hardcoded plist) is rebuilt rather than trusted.
|
|
400
|
+
const key = `${manifest.release} ios-min=${declaredMinOS}`;
|
|
347
401
|
if (isFresh(out, key)) {
|
|
348
402
|
console.log(`[pdfium] ios: ${manifest.release} already present`);
|
|
349
403
|
return;
|
|
@@ -362,6 +416,22 @@ async function ios() {
|
|
|
362
416
|
extract(await fetchArtifact(name), dir);
|
|
363
417
|
slices[name] = dir;
|
|
364
418
|
}
|
|
419
|
+
// Every slice must have been built for the floor the manifest declares. A
|
|
420
|
+
// mismatch means the release was bumped without re-reading its args.gn: fix the
|
|
421
|
+
// manifest (and tell every consumer, whose deployment target must be >= it).
|
|
422
|
+
for (const name of Object.keys(slices)) {
|
|
423
|
+
const actual = machOMinimumOS(join(slices[name], 'lib', 'libpdfium.dylib'));
|
|
424
|
+
if (actual !== declaredMinOS) {
|
|
425
|
+
throw new Error(
|
|
426
|
+
`${name} in ${manifest.release} was built for iOS ${actual}, but ` +
|
|
427
|
+
`pdfium-manifest.json declares iosMinimumOSVersion "${declaredMinOS}". ` +
|
|
428
|
+
'Update the manifest to the real floor; consumers must raise their ' +
|
|
429
|
+
'deployment target to match before they can ship this release.',
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
console.log(`[pdfium] ios: every slice is built for iOS ${declaredMinOS}`);
|
|
434
|
+
|
|
365
435
|
copyHeaders(
|
|
366
436
|
join(slices['ios-device-arm64'], 'include'),
|
|
367
437
|
join(out, 'include'),
|
|
@@ -393,12 +463,14 @@ async function ios() {
|
|
|
393
463
|
deviceFramework,
|
|
394
464
|
join(out, 'include'),
|
|
395
465
|
'iPhoneOS',
|
|
466
|
+
declaredMinOS,
|
|
396
467
|
);
|
|
397
468
|
makeIosFramework(
|
|
398
469
|
simulatorFat,
|
|
399
470
|
simulatorFramework,
|
|
400
471
|
join(out, 'include'),
|
|
401
472
|
'iPhoneSimulator',
|
|
473
|
+
declaredMinOS,
|
|
402
474
|
);
|
|
403
475
|
|
|
404
476
|
const xcframework = join(out, 'PDFium.xcframework');
|
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
"release": "chromium/8044",
|
|
4
4
|
"version": "144.0.8044.0",
|
|
5
5
|
"baseUrl": "https://github.com/bblanchon/pdfium-binaries/releases/download",
|
|
6
|
+
"//iosMinimumOSVersion": "THE iOS FLOOR THIS RELEASE'S DYLIB WAS BUILT FOR (`ios_deployment_target` in the tarball's args.gn, `minos` in the Mach-O LC_BUILD_VERSION). It is declared here, not discovered, so that bumping the release forces whoever bumps it to look: fetch-pdfium.mjs reads the real value out of every iOS slice with `vtool` and refuses to assemble the framework if any slice disagrees with this line. The same value is written into PDFium.framework/Info.plist as MinimumOSVersion — App Store processing rejects a framework whose plist claims a lower floor than its binary (ITMS-90208) — and is the floor PdfCanvas.podspec declares, so a host app whose deployment target is lower fails at `pod install` with a CocoaPods platform error instead of at upload. bblanchon pins 17.0 for non-V8 builds since 2026-08-14; before that the floor tracked Chromium's default and was never below 17.4 in 2025-2026. A host that must ship below this value needs a PDFium built with a lower ios_deployment_target, not a smaller number here.",
|
|
7
|
+
"iosMinimumOSVersion": "17.0",
|
|
6
8
|
"//artifacts": "Keyed by the bblanchon artifact name minus the `pdfium-` prefix and `.tgz` suffix. The android/ios/mac/win/linux entries are the plain (non-V8, non-XFA) builds: this package runs no JavaScript inside a PDF and XFA forms are out of scope, and the V8 builds are ~4x the size for nothing.",
|
|
7
9
|
"artifacts": {
|
|
8
10
|
"android-arm": {
|