mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 11:08:48 -06:00
An A4 page (595.28×841.89 pt) rendered 595×841 at 72 dpi, where pdftoppm gives 596×842: the whole-page render passed hayro no size, and hayro floors the scaled page size. Content touching the right or bottom edge lost its last partial pixel at every resolution. - render: new device_pixels(pt, scale) rounds up (ignoring float noise up to 1/100 px, so Letter at 150 dpi stays 1275 px). The untiled render passes it to hayro as the width and height, still within MAX_SIDE. The partial edge pixels are anti-aliased against the white page, as the clip to the crop box already does. - ui-egui: the canvas tile grid uses the same helper instead of round(), so tiled pages get tiles for the partial row and column too. - No change to vendored hayro. Regression test: a fractional MediaBox renders 596×842 with partly covered edge pixels, 2481 px wide at 300 dpi, and a corner tile matches the whole-page render. It failed (595×841) before the fix. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
Brandon Thomas
parent
4454af3742
commit
9a1bf44e07
@@ -16,7 +16,9 @@ pub use inspect::{
|
||||
Annotation, Attachment, AttachmentSource, DocInfo, Field, FieldKind, FontInfo, Layer, Link, LinkTarget, OutlineItem, PageInfo, Xfa,
|
||||
attachment_data, inspect, pretty_date,
|
||||
};
|
||||
pub use raster::{MAX_PIXELS, MAX_SIDE, PageRenderer, RenderConfig, RenderPool, RenderRequest, RenderedPage, RequestKind, Tile, effective_scale};
|
||||
pub use raster::{
|
||||
MAX_PIXELS, MAX_SIDE, PageRenderer, RenderConfig, RenderPool, RenderRequest, RenderedPage, RequestKind, Tile, device_pixels, effective_scale,
|
||||
};
|
||||
pub use text::{PageText, TextGlyph};
|
||||
|
||||
/// Errors surfaced to the user when a document cannot be opened.
|
||||
|
||||
@@ -115,6 +115,15 @@ pub fn effective_scale(width_pt: f32, height_pt: f32, scale: f32) -> f32 {
|
||||
if capped.is_finite() && capped > 0.0 { capped } else { by_side.min(by_area).max(f32::MIN_POSITIVE) }
|
||||
}
|
||||
|
||||
/// Device pixels that cover `pt` points at `scale`: rounded up, as poppler's `pdftoppm` does, so
|
||||
/// the last partial row or column of a page is drawn (anti-aliased against the background) rather
|
||||
/// than cut off. Float noise up to 1/100 px is not rounded up, so 612 pt at 150 dpi stays 1275 px.
|
||||
/// At least 1; saturates instead of overflowing.
|
||||
pub fn device_pixels(pt: f32, scale: f32) -> u32 {
|
||||
let px = pt * scale - 0.01;
|
||||
if px.is_finite() { px.ceil().max(1.0) as u32 } else { 1 }
|
||||
}
|
||||
|
||||
/// Render one page with a caller-owned parser and cache. Panics inside the renderer are caught
|
||||
/// and reported as `Err((message, panicked))`.
|
||||
type Output = (u32, u32, Vec<u8>, Option<Arc<crate::text::PageText>>);
|
||||
@@ -151,7 +160,10 @@ fn render_page<'a>(pdf: &'a Pdf, cache: &RenderCache<'a>, settings: &Interpreter
|
||||
}
|
||||
None => {
|
||||
let scale = effective_scale(w, h, req.scale);
|
||||
RenderSettings { x_scale: scale, y_scale: scale, bg_color: WHITE, ..Default::default() }
|
||||
// hayro floors the size when none is given, losing the partial edge pixels. The
|
||||
// scale keeps each side within MAX_SIDE (give or take float noise), so it fits u16.
|
||||
let side = |pt: f32| device_pixels(pt, scale).min(MAX_SIDE as u32) as u16;
|
||||
RenderSettings { x_scale: scale, y_scale: scale, width: Some(side(w)), height: Some(side(h)), bg_color: WHITE, ..Default::default() }
|
||||
}
|
||||
};
|
||||
let pixmap = render(page, cache, settings, &rs);
|
||||
@@ -560,6 +572,40 @@ trailer << /Root 1 0 R >>
|
||||
assert!((14_400.0 * s).powi(2) <= MAX_PIXELS * 1.01);
|
||||
}
|
||||
|
||||
/// Issue #102: an A4 page (595.28×841.89 pt) rendered 595×841 at 72 dpi, dropping the last
|
||||
/// partial row and column. Like pdftoppm it is now 596×842, the edge pixels partly covered.
|
||||
#[test]
|
||||
fn fractional_page_sizes_round_up() {
|
||||
let pdf = b"%PDF-1.4
|
||||
1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj
|
||||
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj
|
||||
3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 595.28 841.89] /Contents 4 0 R >> endobj
|
||||
4 0 obj << /Length 31 >> stream
|
||||
0 0 1 rg 0 0 595.28 841.89 re f
|
||||
endstream endobj
|
||||
trailer << /Root 1 0 R >>
|
||||
%%EOF";
|
||||
let mut r = PageRenderer::new(Arc::new(pdf.to_vec()), RenderConfig::default());
|
||||
let req = RenderRequest { page: 0, kind: RequestKind::Pixels, tile: None, scale: 1.0, tag: 0 };
|
||||
let p = r.render(req);
|
||||
assert!(p.error.is_none(), "{:?}", p.error);
|
||||
assert_eq!((p.width, p.height), (596, 842));
|
||||
let px = |x: u32, y: u32| p.rgba[((y * p.width + x) * 4) as usize..][..4].to_vec();
|
||||
assert_eq!(px(594, 840), vec![0, 0, 255, 255]);
|
||||
// The partial column and row blend the fill into the white page.
|
||||
for (x, y) in [(595, 400), (300, 841), (595, 841)] {
|
||||
let c = px(x, y);
|
||||
assert!(c[0] < 255 && c[2] == 255, "({x}, {y}) is not partly covered: {c:?}");
|
||||
}
|
||||
assert_eq!(r.render(RenderRequest { scale: 300.0 / 72.0, ..req }).width, 2481);
|
||||
// A tile over the corner draws the same edge pixels.
|
||||
let t = r.render(RenderRequest { tile: Some(Tile { x: 594, y: 840, w: 2, h: 2 }), ..req });
|
||||
assert_eq!((t.width, t.height), (2, 2));
|
||||
assert_eq!(&t.rgba[12..16], &px(595, 841)[..]);
|
||||
// Float noise doesn't add a row: Letter at 150 dpi is 1275 px wide.
|
||||
assert_eq!((device_pixels(612.0, 150.0 / 72.0), device_pixels(0.2, 1.0), device_pixels(f32::NAN, 1.0)), (1275, 1, 1));
|
||||
}
|
||||
|
||||
/// A checkbox whose `/AP /N` is a state dictionary must draw the `/AS` state; a NoView
|
||||
/// annotation must not draw at all. (Regression test for the vendored hayro patch.)
|
||||
#[test]
|
||||
|
||||
@@ -10,7 +10,7 @@ use std::sync::Arc;
|
||||
|
||||
use egui::{Align2, Color32, CornerRadius, Pos2, Rect, Sense, Stroke, TextureHandle, TextureOptions, Vec2, pos2, vec2};
|
||||
use pdfcraft_engine::{DocId, Edit};
|
||||
use pdfcraft_render::{DocInfo, LinkTarget, PageText, RenderPool, RenderRequest, RequestKind, Tile};
|
||||
use pdfcraft_render::{DocInfo, LinkTarget, PageText, RenderPool, RenderRequest, RequestKind, Tile, device_pixels};
|
||||
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{PdfCraftApp, QuickTool, RightPanel, comments, icons, widgets};
|
||||
@@ -1135,8 +1135,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
}
|
||||
if tiled && r.intersects(visible) {
|
||||
// Device-pixel geometry of the scaled page, and the visible part of it
|
||||
// (found by mapping the visible screen corners back into the page).
|
||||
let (dw, dh) = ((pw_pt * scale).round() as u32, (ph_pt * scale).round() as u32);
|
||||
// (found by mapping the visible screen corners back into the page). Sides round
|
||||
// up as whole-page rasters do, so the last partial row and column get tiles.
|
||||
let (dw, dh) = (device_pixels(pw_pt, scale), device_pixels(ph_pt, scale));
|
||||
let vis = r.intersect(visible);
|
||||
let corners = [vis.left_top(), vis.right_top(), vis.right_bottom(), vis.left_bottom()].map(|c| xf.screen_to_norm(c));
|
||||
let (u0, u1) = corners.iter().fold((1.0f32, 0.0f32), |(a, b), c| (a.min(c.0), b.max(c.0)));
|
||||
|
||||
Reference in New Issue
Block a user