summaryrefslogtreecommitdiff
path: root/test
diff options
context:
space:
mode:
Diffstat (limited to 'test')
-rw-r--r--test/pdf.zig168
1 files changed, 167 insertions, 1 deletions
diff --git a/test/pdf.zig b/test/pdf.zig
index fe084cc1..6864f247 100644
--- a/test/pdf.zig
+++ b/test/pdf.zig
@@ -730,6 +730,7 @@ test "MuPDF pane renders, navigates, searches, and round-trips its page" {
const request = panes.Pdf.renderRequest(
panes.Pdf.paneViewport(p, pane) orelse return error.MissingPdfViewport,
pdf_raster_policy,
+ pane.pdf.?.fit,
);
try std.testing.expectEqual(pdf_raster_policy.dpi, request.dpi);
try std.testing.expectEqual(pdf_raster_policy.max_dimension, request.max_dimension);
@@ -1119,6 +1120,171 @@ test "a fling's banded pages show the reader exactly what whole pages would" {
}
}
+/// MuPDF clips the edges of a path to the pixmap, and an edge that crosses a
+/// band's clip is stepped from a different start: along its whole length in
+/// the band, its antialiased pixels can be a level or two off the whole
+/// page's. Never more.
+const band_tolerance: u8 = 4;
+
+fn expectNearRows(expected: []const u8, got: []const u8) !void {
+ try std.testing.expectEqual(expected.len, got.len);
+ var worst: u8 = 0;
+ for (expected, got) |a, b| worst = @max(worst, if (a > b) a - b else b - a);
+ if (worst > band_tolerance) {
+ std.debug.print("band rows are {d} levels off the whole page's\n", .{worst});
+ return error.TestExpectedEqual;
+ }
+}
+
+test "a tall page is drawn at the display's resolution, in bands that are the whole page's rows" {
+ if (!pdf_enabled or platform == .web) return;
+
+ // The page is fifteen A4 heights tall. Squeezed whole into the raster
+ // budget it was a third as wide as the pane and blown up, blurred; it is
+ // drawn instead at the pane's own resolution, a band around the screen at
+ // a time, and every band row must be the row a whole-page render makes
+ // (to `band_tolerance`), and the same row however it was scrolled to.
+ const gpa = std.testing.allocator;
+ var tmp = std.testing.tmpDir(.{});
+ defer tmp.cleanup();
+ const bytes = try pdf_impl.makeTallTestPdf(gpa);
+ defer gpa.free(bytes);
+ try tmp.dir.writeFile(std.testing.io, .{ .sub_path = "tall.pdf", .data = bytes });
+ var dir_buf: [4096]u8 = undefined;
+ const dir_len = try tmp.dir.realPath(std.testing.io, &dir_buf);
+ const path = try std.fmt.allocPrint(gpa, "{s}/tall.pdf", .{dir_buf[0..dir_len]});
+ defer gpa.free(path);
+ const p = try Pardes.init(gpa, .{ .file = path, .cols = 120, .rows = 40 });
+ defer p.deinit();
+ p.native_images = true;
+ const pane = p.panes[0].?;
+ const pv = &pane.pdf.?;
+ var frame = std.heap.ArenaAllocator.init(gpa);
+ defer frame.deinit();
+ _ = try p.render(frame.allocator());
+
+ const viewport = panes.Pdf.paneViewport(p, pane) orelse return error.MissingPdfViewport;
+ const request = panes.Pdf.renderRequest(viewport, pdf_raster_policy, pv.fit);
+ const shape = try pv.document.measureRenderAt(0, request);
+ if (pdf_raster_policy.match_viewport) {
+ // The rendered scale IS the screen's: one raster pixel per pixel of
+ // the pane's width, so nothing is scaled up.
+ try std.testing.expectEqual(@as(usize, viewport.pixel_w), shape.width);
+ } else {
+ // Kitty's policy is a dpi, kept low for the wire; the tall page gets
+ // it as every other page does, not a quarter of it.
+ try std.testing.expectEqual(
+ @as(usize, @intFromFloat(pdf_impl.tall_test_page.width)) * pdf_raster_policy.dpi / 72,
+ shape.width,
+ );
+ }
+ try std.testing.expect(panes.Pdf.isBanded(shape));
+
+ // The reference: the whole page in one render, before highlights and
+ // tint, and tinted as the pane tints.
+ const plain = try gpa.alloc(u8, shape.len);
+ defer gpa.free(plain);
+ try pv.document.renderIntoAt(0, request, shape, shape.wholePage(), &.{}, plain);
+ const whole = try gpa.dupe(u8, plain);
+ defer gpa.free(whole);
+ try pdf_impl.tintRgba(whole, pv.tint, panes.Pdf.tintColors(p));
+
+ const max_scroll = @as(f64, @floatFromInt(pv.document_height - viewport.pixel_h));
+ var middle: []u8 = &.{};
+ defer gpa.free(middle);
+ var middle_y: usize = 0;
+ for ([_]f64{ 0, @round(max_scroll / 2), max_scroll }, 0..) |at, step| {
+ _ = panes.Pdf.scrollPane(p, pane, at - pv.document_scroll_y);
+ pv.scroll_travel = 0; // read, not flung
+ _ = frame.reset(.retain_capacity);
+ const surface = try p.render(frame.allocator());
+ try std.testing.expectEqual(@as(usize, 1), surface.nimages);
+ const place = surface.images[0].?;
+ const geometry = place.native.geometry orelse return error.MissingPdfGeometry;
+ const raster = panes.Pdf.rasterForPage(pv, 0) orelse return error.MissingPdfRaster;
+ // Bounded: a band, never the page, and within one buffer's budget.
+ try std.testing.expectEqual(shape.width, place.iw);
+ try std.testing.expect(raster.band_h < raster.ih);
+ try std.testing.expect(place.rgba.len <= pdf_impl.max_owned_raster_bytes);
+ // Prefetched: a screenful beyond what is shown, both ways, where the
+ // page goes on.
+ const seen_top = raster.band_y + geometry.src.y;
+ const seen_end = seen_top + geometry.src.h;
+ try std.testing.expect(raster.band_y <= seen_top -| geometry.src.h);
+ try std.testing.expect(raster.band_y + raster.band_h >= @min(raster.ih, seen_end + geometry.src.h));
+ // ...and every row of the band is the whole page's row.
+ const stride = shape.stride;
+ try expectNearRows(whole[raster.band_y * stride ..][0 .. raster.band_h * stride], place.rgba);
+ if (step == 1) {
+ middle = try gpa.dupe(u8, place.rgba);
+ middle_y = raster.band_y;
+ }
+ }
+
+ // Straight back to the middle from the foot: the rows rendered afresh
+ // are the very rows rendered on the way down, so no seam moves with the
+ // path a reader took.
+ _ = panes.Pdf.scrollPane(p, pane, @round(max_scroll / 2) - pv.document_scroll_y);
+ pv.scroll_travel = 0;
+ _ = frame.reset(.retain_capacity);
+ const again = (try p.render(frame.allocator())).images[0].?;
+ const again_y = panes.Pdf.rasterForPage(pv, 0).?.band_y;
+ const stride = shape.stride;
+ const common_top = @max(middle_y, again_y);
+ const common_end = @min(middle_y + middle.len / stride, again_y + again.rgba.len / stride);
+ try std.testing.expect(common_end > common_top);
+ try std.testing.expectEqualSlices(
+ u8,
+ middle[(common_top - middle_y) * stride .. (common_end - middle_y) * stride],
+ again.rgba[(common_top - again_y) * stride .. (common_end - again_y) * stride],
+ );
+ _ = panes.Pdf.scrollPane(p, pane, max_scroll - pv.document_scroll_y);
+ pv.scroll_travel = 0;
+ _ = frame.reset(.retain_capacity);
+ const foot = try gpa.dupe(u8, (try p.render(frame.allocator())).images[0].?.rgba);
+ defer gpa.free(foot);
+
+ // At the foot of the page, a search hit: its highlight lands on exactly
+ // the rows and pixels it would on the whole page, and a pointer over the
+ // word finds the word.
+ try pv.setSearchQuery(gpa, "bottommost");
+ _ = frame.reset(.retain_capacity);
+ const lit = try p.render(frame.allocator());
+ const place = lit.images[0].?;
+ const raster = panes.Pdf.rasterForPage(pv, 0) orelse return error.MissingPdfRaster;
+ const highlights = try panes.Pdf.buildHighlights(pv, frame.allocator(), panes.Pdf.highlightInput(p, 0, pane));
+ const marks = highlights.forPage(0);
+ try std.testing.expect(marks.len > 0);
+ const rows = try pv.document.highlightRows(0, request, marks);
+ try std.testing.expect(rows.start >= raster.band_y and rows.end <= raster.band_y + raster.band_h);
+ // Exactly the highlight's rows changed, and nothing else...
+ try std.testing.expectEqual(foot.len, place.rgba.len);
+ const from = (rows.start - raster.band_y) * stride;
+ const to = (rows.end - raster.band_y) * stride;
+ try std.testing.expectEqualSlices(u8, foot[0..from], place.rgba[0..from]);
+ try std.testing.expectEqualSlices(u8, foot[to..], place.rgba[to..]);
+ try std.testing.expect(!std.mem.eql(u8, foot[from..to], place.rgba[from..to]));
+ // ...to what the whole page painted with them is.
+ const expected = try gpa.dupe(u8, plain[rows.start * stride .. rows.end * stride]);
+ defer gpa.free(expected);
+ try pv.document.paintHighlightsAt(0, request, shape, shape.band(rows.start, rows.end - rows.start), marks, expected);
+ try pdf_impl.tintRgba(expected, pv.tint, panes.Pdf.tintColors(p));
+ try expectNearRows(expected, place.rgba[from..to]);
+
+ const quad = pv.search_results.?.quads[0].quad;
+ const left = @min(quad.ul.x, quad.ll.x);
+ const right = @max(quad.ur.x, quad.lr.x);
+ const top = @min(quad.ul.y, quad.ur.y);
+ const bottom = @max(quad.ll.y, quad.lr.y);
+ const rect = p.rects[0];
+ var inside: usize = 0;
+ for (rect.y..rect.y + rect.h) |row| for (rect.x..rect.x + rect.w) |col| {
+ const point = panes.Pdf.panePointAtPage(p, pane, 0, @intCast(col), @intCast(row), false) orelse continue;
+ if (point.x >= left and point.x <= right and point.y >= top and point.y <= bottom) inside += 1;
+ };
+ try std.testing.expect(inside > 0);
+}
+
/// The pixels a backend samples out of one placement: the source rectangle,
/// row by row, at the texture's own stride. Test-only, and the one operation
/// that makes "same picture" mean something when the textures differ in shape.
@@ -1306,7 +1472,7 @@ test "PDF continuous strip renders every intersecting short page" {
const viewport = panes.Pdf.ensurePaneLayout(p, pane).?;
const visible = panes.Pdf.visiblePages(pv, viewport);
try std.testing.expectEqual(pv.page_count, visible.len);
- const request = panes.Pdf.renderRequest(viewport, pdf_raster_policy);
+ const request = panes.Pdf.renderRequest(viewport, pdf_raster_policy, pv.fit);
try std.testing.expect(pv.page_count <= pv.rasters.len);
for (0..pv.page_count) |page| {
const rgba = try gpa.alloc(u8, @as(usize, viewport.pixel_w) * 4);