@reekon-tools/react-native-pdf-canvas 0.2.1 → 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.
@@ -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.2.1",
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",