perf: simplify stroke outlines before PDF export

Each stroke mesh has about 16 points per pen sample, and every one of them
went into the PDF as a polygon point: a full handwritten page (1650 strokes)
took 2.9 s to export, and PNG export and copying a selection as an image
took as long, because both go through the PDF. The outline is now simplified
with a closed Douglas-Peucker that measures against segments, at most 0.01 mm
from the drawn outline. The same page now takes 0.44 s (PNG 0.55 s), the PDF
shrinks from 10.7 MB to 1.4 MB, and the rendered PNG differs only in edge
anti-aliasing (max 21/255). INK_NOTES_EXACT_OUTLINES=1 exports every point
as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
haraldandClaude Opus 5.5 committed 2026-10-27 08:45:17 +01:00
1 parent 330c29193f
commit 955eaa4095
4 files changed
+161 -17

No files matched your search

+30 -15
View File
@@ -1222,21 +1222,25 @@ fn render_stroke_polygon(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
// Geschlossener Pfad um den Ribbon-Rand:
// Left[0] -> Left[1] -> ... -> Left[n-1] -> Right[n-1] -> Right[n-2] -> ... -> Right[0]
let mut ring = Vec::with_capacity(n * 2);
// 1. Linke Hüllkurve vorwärts
for i in 0..n {
let v = stroke.vertices[i * 2];
let (x, y) = page_to_pdf_mm(v.position[0], v.position[1]);
ring.push((Point::new(Mm(x), Mm(y)), false));
}
// 2. Rechte Hüllkurve rückwärts
for i in (0..n).rev() {
let v = stroke.vertices[i * 2 + 1];
let (x, y) = page_to_pdf_mm(v.position[0], v.position[1]);
ring.push((Point::new(Mm(x), Mm(y)), false));
}
let outline: Vec<glam::Vec2> = (0..n)
.map(|i| stroke.vertices[i * 2].position)
.chain((0..n).rev().map(|i| stroke.vertices[i * 2 + 1].position))
.map(glam::Vec2::from)
.collect();
// The mesh has about 16 points per pen sample, far more than the eye (or a printer) can
// tell apart; fewer points make the PDF, PNG export and copying as an image much faster
let outline = if exact_outlines() {
outline
} else {
crate::geometry::simplify_closed_outline(&outline, OUTLINE_TOLERANCE)
};
let ring: Vec<(Point, bool)> = outline
.iter()
.map(|p| {
let (x, y) = page_to_pdf_mm(p.x, p.y);
(Point::new(Mm(x), Mm(y)), false)
})
.collect();
let fill_color = Color::Rgb(Rgb::new(
stroke.color[0],
@@ -1254,6 +1258,17 @@ fn render_stroke_polygon(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
layer.add_polygon(poly);
}
/// Largest deviation of an exported stroke outline from the drawn one, in page units
/// (0.01 mm: a tenth of a pixel at 200 dpi).
pub const OUTLINE_TOLERANCE: f32 = 0.1;
/// `INK_NOTES_EXACT_OUTLINES=1` exports every point of the stroke outlines, as before this change
/// (fallback in case the simplified outlines ever look wrong).
fn exact_outlines() -> bool {
static EXACT: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
*EXACT.get_or_init(|| std::env::var_os("INK_NOTES_EXACT_OUTLINES").is_some_and(|v| v != "0"))
}
/// Strategie B: Kubische Bézier-Kurven aus den Abtastpunkten via printpdf::Line.
fn render_stroke_bezier(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
if stroke.points.is_empty() {