diff options
Diffstat (limited to 'test')
| -rw-r--r-- | test/pdf.zig | 168 |
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); |
