forked from 1b-agent/opencadstudio-upstream
Merge pull request #390 from KevinGriffin-new/fix/pdf-export-dimensions
Render SDF text as vectors in PDF/print export (#385). Text became SDF-only, so all text — standalone TEXT/MTEXT and dimension text — rode WireModel::text_verts as GPU glyph quads that the CPU PDF exporter never sampled. Reconstruct the vector geometry at export time via a new GlyphAtlas::export_table() keyed by the quads' atlas uv_min.
This commit is contained in:
commit
5bdfe483da
3 files changed
+367
No files matched your search
@@ -322,6 +322,13 @@ fn build_pdf(
|
|||||||
flush_line(&mut ops, &segment);
|
flush_line(&mut ops, &segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Text (SDF glyph quads) — re-emitted as vector strokes / fills. Text now
|
||||||
|
// renders on-screen only as textured SDF quads (`wire.text_verts`), which
|
||||||
|
// this CPU exporter can't sample, so without this pass all text — including
|
||||||
|
// dimension text — is missing from the PDF (issue #385). Drawn after the
|
||||||
|
// wires (on top) and under the same rotation/scale/clip CTM.
|
||||||
|
emit_text(&mut ops, wires, ox, oy);
|
||||||
|
|
||||||
if needs_state {
|
if needs_state {
|
||||||
ops.push(Op::RestoreGraphicsState);
|
ops.push(Op::RestoreGraphicsState);
|
||||||
}
|
}
|
||||||
@@ -507,6 +514,164 @@ fn emit_hatch(ops: &mut Vec<Op>, hatch: &HatchModel, ox: f32, oy: f32) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Text (SDF glyph quads → vector strokes / fills) ────────────────────────
|
||||||
|
|
||||||
|
/// Absolute world XY of a glyph vertex (double-single high + low parts folded).
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn glyph_world_xy(v: &crate::scene::pipeline::text_gpu::TextVertex) -> [f32; 2] {
|
||||||
|
[v.pos[0] + v.pos_low[0], v.pos[1] + v.pos_low[1]]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adapt a text colour to the white sheet, mirroring the wire/hatch passes:
|
||||||
|
/// near-white / near-yellow (colour-7-on-white) → black, near-cyan → dark blue.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn adapt_text_color([r, g, b]: [f32; 3]) -> [f32; 3] {
|
||||||
|
let is_light = r > 0.80 && g > 0.80 && b > 0.80;
|
||||||
|
let is_yellow = r > 0.80 && g > 0.70 && b < 0.30;
|
||||||
|
let is_cyan = r < 0.30 && g > 0.70 && b > 0.70;
|
||||||
|
if is_light || is_yellow {
|
||||||
|
[0.0, 0.0, 0.0]
|
||||||
|
} else if is_cyan {
|
||||||
|
[0.0, 0.15, 0.50]
|
||||||
|
} else {
|
||||||
|
[r, g, b]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Re-emit every wire's SDF text as vector geometry.
|
||||||
|
///
|
||||||
|
/// Each visible glyph rides on `wire.text_verts` as one 6-vertex quad (two
|
||||||
|
/// triangles) whose corners are the glyph's atlas `plane` rect run through the
|
||||||
|
/// text transform. We recover the glyph's outline / fill from the atlas by the
|
||||||
|
/// quad's `uv_min` and map it into that quad by affine interpolation of the
|
||||||
|
/// plane rect — so a stroke (LFF) font emits polylines and a filled TrueType
|
||||||
|
/// glyph emits filled triangles, exactly where the SDF quad sits.
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn emit_text(ops: &mut Vec<Op>, wires: &[WireModel], ox: f32, oy: f32) {
|
||||||
|
use crate::scene::text::sdf_atlas;
|
||||||
|
|
||||||
|
if wires.iter().all(|w| w.text_verts.is_empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Snapshot the atlas' baked-glyph geometry once; drop the lock before use.
|
||||||
|
let (table, solid_key) = {
|
||||||
|
let Ok(atlas) = sdf_atlas::text_atlas().lock() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
(atlas.export_table(), sdf_atlas::uv_key(atlas.solid_uv()))
|
||||||
|
};
|
||||||
|
|
||||||
|
const MM_TO_PT: f32 = 2.834645;
|
||||||
|
// Match the wire pass: screen-px weight → true physical points.
|
||||||
|
const LW_PX_TO_PT: f32 = MM_TO_PT / ((96.0 / 25.4) * 2.0);
|
||||||
|
|
||||||
|
for wire in wires {
|
||||||
|
let verts = &wire.text_verts;
|
||||||
|
if verts.is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let lw_pt = (wire.line_weight_px * LW_PX_TO_PT).max(0.1);
|
||||||
|
|
||||||
|
let mut gi = 0;
|
||||||
|
while gi + 6 <= verts.len() {
|
||||||
|
let quad = &verts[gi..gi + 6];
|
||||||
|
gi += 6;
|
||||||
|
|
||||||
|
let a = quad[0].color[3];
|
||||||
|
if a < 0.01 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let [r, g, b] =
|
||||||
|
adapt_text_color([quad[0].color[0], quad[0].color[1], quad[0].color[2]]);
|
||||||
|
|
||||||
|
// Quad corners in world XY: verts run [bl, br, tr, bl, tr, tl].
|
||||||
|
let bl = glyph_world_xy(&quad[0]);
|
||||||
|
let br = glyph_world_xy(&quad[1]);
|
||||||
|
let tr = glyph_world_xy(&quad[2]);
|
||||||
|
let tl = glyph_world_xy(&quad[5]);
|
||||||
|
// `tl` carries uv = (uv_min.x, uv_min.y) — the atlas tile key.
|
||||||
|
let key = sdf_atlas::uv_key([quad[5].uv[0], quad[5].uv[1]]);
|
||||||
|
|
||||||
|
let point = |wx: f32, wy: f32| Point::new(Mm(wx + ox), Mm(wy + oy));
|
||||||
|
|
||||||
|
if let Some(ge) = table.get(&key) {
|
||||||
|
// Affine basis of the quad: plane_min → bl, +x → br, +y → tl.
|
||||||
|
let (pmin, pmax) = (ge.plane_min, ge.plane_max);
|
||||||
|
let (sx, sy) = (pmax[0] - pmin[0], pmax[1] - pmin[1]);
|
||||||
|
if sx.abs() < 1e-9 || sy.abs() < 1e-9 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let map = |p: [f32; 2]| -> Point {
|
||||||
|
let u = (p[0] - pmin[0]) / sx;
|
||||||
|
let v = (p[1] - pmin[1]) / sy;
|
||||||
|
let wx = bl[0] + u * (br[0] - bl[0]) + v * (tl[0] - bl[0]);
|
||||||
|
let wy = bl[1] + u * (br[1] - bl[1]) + v * (tl[1] - bl[1]);
|
||||||
|
point(wx, wy)
|
||||||
|
};
|
||||||
|
|
||||||
|
if !ge.fill_tris.is_empty() {
|
||||||
|
// Filled TrueType glyph: one filled triangle per triple.
|
||||||
|
ops.push(Op::SetFillColor {
|
||||||
|
col: Color::Rgb(Rgb { r, g, b, icc_profile: None }),
|
||||||
|
});
|
||||||
|
for tri in ge.fill_tris.chunks_exact(3) {
|
||||||
|
ops.push(Op::DrawPolygon {
|
||||||
|
polygon: Polygon {
|
||||||
|
rings: vec![PolygonRing {
|
||||||
|
points: tri
|
||||||
|
.iter()
|
||||||
|
.map(|&p| LinePoint { p: map(p), bezier: false })
|
||||||
|
.collect(),
|
||||||
|
}],
|
||||||
|
mode: PaintMode::Fill,
|
||||||
|
winding_order: WindingOrder::NonZero,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Stroke (LFF pen) font or hollow glyph: polylines.
|
||||||
|
ops.push(Op::SetOutlineColor {
|
||||||
|
col: Color::Rgb(Rgb { r, g, b, icc_profile: None }),
|
||||||
|
});
|
||||||
|
ops.push(Op::SetOutlineThickness { pt: Pt(lw_pt) });
|
||||||
|
for stroke in &ge.strokes {
|
||||||
|
if stroke.len() < 2 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
ops.push(Op::DrawLine {
|
||||||
|
line: Line {
|
||||||
|
points: stroke
|
||||||
|
.iter()
|
||||||
|
.map(|&p| LinePoint { p: map(p), bezier: false })
|
||||||
|
.collect(),
|
||||||
|
is_closed: false,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if key == solid_key {
|
||||||
|
// Decoration bar (underline / overline / strike): the quad is a
|
||||||
|
// solid-texel rectangle — fill it directly from its corners.
|
||||||
|
ops.push(Op::SetFillColor {
|
||||||
|
col: Color::Rgb(Rgb { r, g, b, icc_profile: None }),
|
||||||
|
});
|
||||||
|
ops.push(Op::DrawPolygon {
|
||||||
|
polygon: Polygon {
|
||||||
|
rings: vec![PolygonRing {
|
||||||
|
points: [bl, br, tr, tl]
|
||||||
|
.iter()
|
||||||
|
.map(|&c| LinePoint { p: point(c[0], c[1]), bezier: false })
|
||||||
|
.collect(),
|
||||||
|
}],
|
||||||
|
mode: PaintMode::Fill,
|
||||||
|
winding_order: WindingOrder::NonZero,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -536,4 +701,84 @@ mod tests {
|
|||||||
assert!(bytes.starts_with(b"%PDF"), "not a PDF");
|
assert!(bytes.starts_with(b"%PDF"), "not a PDF");
|
||||||
assert!(bytes.len() > 200, "suspiciously small: {}", bytes.len());
|
assert!(bytes.len() > 200, "suspiciously small: {}", bytes.len());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build a WireModel carrying the SDF glyph quads for `text` in the embedded
|
||||||
|
// "txt" stroke font, laid out into the process-wide atlas emit_text reads.
|
||||||
|
fn text_wire(text: &str, origin: [f64; 3]) -> WireModel {
|
||||||
|
use crate::scene::pipeline::text_gpu::push_glyph_vertices;
|
||||||
|
use crate::scene::text::{glyph_quads::layout_glyph_quads, sdf_atlas};
|
||||||
|
let quads = {
|
||||||
|
let mut atlas = sdf_atlas::text_atlas().lock().unwrap();
|
||||||
|
layout_glyph_quads(&mut atlas, 10.0, 0.0, 1.0, 0.0, 1.0, "txt", false, text)
|
||||||
|
};
|
||||||
|
assert!(!quads.is_empty(), "stroke glyphs laid out for {text:?}");
|
||||||
|
let mut verts = Vec::new();
|
||||||
|
push_glyph_vertices(&mut verts, &quads, origin, 1.0, [1.0, 0.0, 0.0, 1.0], 0.0);
|
||||||
|
WireModel {
|
||||||
|
text_verts: verts,
|
||||||
|
..WireModel::solid("t".into(), Vec::new(), WireModel::WHITE, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regression for #385: SDF text (`text_verts`) must be re-emitted as vector
|
||||||
|
// draw ops. Before the fix the exporter ignored `text_verts` entirely, so a
|
||||||
|
// text-only wire produced no glyph geometry — dimensions/text vanished.
|
||||||
|
#[test]
|
||||||
|
fn sdf_text_emits_vector_ops_within_glyph_bounds() {
|
||||||
|
let origin = [100.0, 50.0, 0.0];
|
||||||
|
let wire = text_wire("AB", origin);
|
||||||
|
|
||||||
|
// World bbox of the glyph quads — every emitted point must land inside.
|
||||||
|
let (mut nx, mut ny, mut xx, mut xy) = (f32::MAX, f32::MAX, f32::MIN, f32::MIN);
|
||||||
|
for v in &wire.text_verts {
|
||||||
|
let (x, y) = (v.pos[0] + v.pos_low[0], v.pos[1] + v.pos_low[1]);
|
||||||
|
nx = nx.min(x);
|
||||||
|
xx = xx.max(x);
|
||||||
|
ny = ny.min(y);
|
||||||
|
xy = xy.max(y);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ops: Vec<Op> = Vec::new();
|
||||||
|
emit_text(&mut ops, std::slice::from_ref(&wire), 0.0, 0.0);
|
||||||
|
|
||||||
|
let lines = ops
|
||||||
|
.iter()
|
||||||
|
.filter(|o| matches!(o, Op::DrawLine { .. }))
|
||||||
|
.count();
|
||||||
|
assert!(lines > 0, "stroke text emitted no polylines (ops: {})", ops.len());
|
||||||
|
|
||||||
|
// Mapped points (Pt = mm × 2.834645, ox/oy = 0) sit within the bbox.
|
||||||
|
const MM_TO_PT: f32 = 2.834645;
|
||||||
|
let eps = 1.0; // mm slack for the SDF plane spread past the ink.
|
||||||
|
for o in &ops {
|
||||||
|
if let Op::DrawLine { line } = o {
|
||||||
|
for lp in &line.points {
|
||||||
|
let (x, y) = (lp.p.x.0 / MM_TO_PT, lp.p.y.0 / MM_TO_PT);
|
||||||
|
assert!(
|
||||||
|
x >= nx - eps && x <= xx + eps && y >= ny - eps && y <= xy + eps,
|
||||||
|
"glyph point ({x},{y}) outside bbox [{nx},{ny},{xx},{xy}]"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// End-to-end: a page whose only content is SDF text produces a larger PDF
|
||||||
|
// than the same page with the text stripped — proving text reaches the file.
|
||||||
|
#[test]
|
||||||
|
fn text_grows_the_pdf_vs_no_text() {
|
||||||
|
let wire = text_wire("HELLO", [20.0, 20.0, 0.0]);
|
||||||
|
let mut blank = wire.clone();
|
||||||
|
blank.text_verts.clear();
|
||||||
|
|
||||||
|
let with_text = build_pdf(&[wire], &[], &[], 210.0, 297.0, 0.0, 0.0, 0, 1.0, None, None);
|
||||||
|
let no_text = build_pdf(&[blank], &[], &[], 210.0, 297.0, 0.0, 0.0, 0, 1.0, None, None);
|
||||||
|
assert!(with_text.starts_with(b"%PDF"));
|
||||||
|
assert!(
|
||||||
|
with_text.len() > no_text.len(),
|
||||||
|
"text did not add content: {} !> {}",
|
||||||
|
with_text.len(),
|
||||||
|
no_text.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -113,6 +113,31 @@ pub struct AtlasEntry {
|
|||||||
pub advance: f32,
|
pub advance: f32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A glyph's vector geometry recovered for CPU export (PDF / print), where the
|
||||||
|
/// SDF atlas texture can't be sampled. All coordinates are in the 9-unit glyph
|
||||||
|
/// space; `plane_min`/`plane_max` is the same tile rect the render quad spans,
|
||||||
|
/// so the exporter can map the geometry into a rendered quad by affine interp.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct GlyphExport {
|
||||||
|
pub plane_min: [f32; 2],
|
||||||
|
pub plane_max: [f32; 2],
|
||||||
|
/// Outline / centreline polylines (glyph units).
|
||||||
|
pub strokes: Vec<Vec<[f32; 2]>>,
|
||||||
|
/// Fill triangulation (glyph units, flat triples). Non-empty only for a
|
||||||
|
/// filled TrueType glyph — the exporter fills these and ignores `strokes`;
|
||||||
|
/// empty means "stroke `strokes`" (LFF pen font, or a hollow TTF glyph).
|
||||||
|
pub fill_tris: Vec<[f32; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pack a glyph quad's `uv_min` corner into a stable hash key for the export
|
||||||
|
/// table below. The two f32 UVs are exact copies of the baked `AtlasEntry`
|
||||||
|
/// values (via `GlyphQuad`), so bit-equality is a reliable identity as long as
|
||||||
|
/// the atlas hasn't been re-scaled by a growth since the quads were built —
|
||||||
|
/// which, for an export of an already-viewed drawing, it hasn't.
|
||||||
|
pub fn uv_key(uv_min: [f32; 2]) -> u64 {
|
||||||
|
((uv_min[0].to_bits() as u64) << 32) | uv_min[1].to_bits() as u64
|
||||||
|
}
|
||||||
|
|
||||||
/// A single-channel SDF glyph atlas plus a shelf packer.
|
/// A single-channel SDF glyph atlas plus a shelf packer.
|
||||||
pub struct GlyphAtlas {
|
pub struct GlyphAtlas {
|
||||||
width: u32,
|
width: u32,
|
||||||
@@ -230,6 +255,34 @@ impl GlyphAtlas {
|
|||||||
entry
|
entry
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Snapshot every baked glyph's vector geometry, keyed by its tile
|
||||||
|
/// `uv_min` ([`uv_key`]). CPU exporters (PDF / print) walk a text run's SDF
|
||||||
|
/// glyph quads, look each quad's `uv_min` up here, and re-emit the glyph as
|
||||||
|
/// vector strokes / fills instead of sampling the SDF texture. Re-resolves
|
||||||
|
/// each entry's outline through `Face` (the same source `get_or_insert`
|
||||||
|
/// baked from), so it reflects the current TEXTFILL mode.
|
||||||
|
pub fn export_table(&self) -> HashMap<u64, GlyphExport> {
|
||||||
|
let mut out = HashMap::with_capacity(self.entries.len());
|
||||||
|
for ((family, ch, bold), entry) in &self.entries {
|
||||||
|
let Some(entry) = entry else { continue };
|
||||||
|
let face = Face::resolve(family);
|
||||||
|
let Some(g) = face.glyph(*ch) else { continue };
|
||||||
|
// Bold LFF glyphs bake with a wider pen but the same centrelines;
|
||||||
|
// the exporter widens the pen itself, so the geometry is shared.
|
||||||
|
let _ = bold;
|
||||||
|
out.insert(
|
||||||
|
uv_key(entry.uv_min),
|
||||||
|
GlyphExport {
|
||||||
|
plane_min: entry.plane_min,
|
||||||
|
plane_max: entry.plane_max,
|
||||||
|
strokes: g.strokes.clone(),
|
||||||
|
fill_tris: g.fill_tris.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
/// Grow the atlas taller (keeping the width, so packed X positions and their
|
/// Grow the atlas taller (keeping the width, so packed X positions and their
|
||||||
/// normalized U stay valid) and rescale every cached entry's V for the new
|
/// normalized U stay valid) and rescale every cached entry's V for the new
|
||||||
/// height. Returns false once the height hits `MAX_ATLAS_PX` — text-heavy
|
/// height. Returns false once the height hits `MAX_ATLAS_PX` — text-heavy
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Regression for #385: text and dimension text must reach the PDF / print
|
||||||
|
// export. From v0.8.2 text renders on-screen only as GPU SDF glyph quads
|
||||||
|
// (`WireModel::text_verts`); the CPU PDF exporter used to draw only wire
|
||||||
|
// stroke `points`, so all text — standalone TEXT/MTEXT and dimension text —
|
||||||
|
// vanished from exported PDFs (present in v0.7.6). This drives the real
|
||||||
|
// entity -> scene.entity_wires() -> export_pdf path and asserts text is
|
||||||
|
// carried to the exporter and lands in the file.
|
||||||
|
use acadrust::entities::{Dimension, DimensionLinear, Text};
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
use acadrust::EntityType;
|
||||||
|
use OpenCADStudio::io::pdf_export::export_pdf;
|
||||||
|
use OpenCADStudio::scene::Scene;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn text_and_dim_reach_pdf_export() {
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
|
||||||
|
// A standalone TEXT entity.
|
||||||
|
let t = Text::with_value("HELLO", Vector3::new(10.0, 10.0, 0.0)).with_height(5.0);
|
||||||
|
scene.add_entity(EntityType::Text(t));
|
||||||
|
|
||||||
|
// A linear dimension — its measurement value is synthesized as SDF text
|
||||||
|
// too, so it exercises the dimension-text path the reporters called out.
|
||||||
|
let dim = DimensionLinear::new(
|
||||||
|
Vector3::new(0.0, 0.0, 0.0),
|
||||||
|
Vector3::new(40.0, 0.0, 0.0),
|
||||||
|
);
|
||||||
|
scene.add_entity(EntityType::Dimension(Dimension::Linear(dim)));
|
||||||
|
|
||||||
|
let wires = scene.entity_wires();
|
||||||
|
|
||||||
|
// The regression symptom was that text produced no exportable geometry.
|
||||||
|
// It must now ride the export wire set as SDF glyph quads.
|
||||||
|
let text_wires = wires.iter().filter(|w| !w.text_verts.is_empty()).count();
|
||||||
|
assert!(
|
||||||
|
text_wires > 0,
|
||||||
|
"no text_verts on the export wire set — text/dim text would be missing from the PDF"
|
||||||
|
);
|
||||||
|
|
||||||
|
// End-to-end: exporting with the text present must produce a valid PDF that
|
||||||
|
// is larger than the same wire set with the text stripped — i.e. the glyph
|
||||||
|
// geometry actually reaches the file.
|
||||||
|
let dir = std::env::temp_dir();
|
||||||
|
let p_text = dir.join("ocs_385_with_text.pdf");
|
||||||
|
export_pdf(&wires, &[], &[], 210.0, 297.0, 0.0, 0.0, 0, 1.0, None, &p_text, None)
|
||||||
|
.expect("export with text");
|
||||||
|
let with_text = std::fs::read(&p_text).expect("read pdf");
|
||||||
|
assert!(with_text.starts_with(b"%PDF"), "not a PDF");
|
||||||
|
|
||||||
|
let stripped: Vec<_> = wires
|
||||||
|
.iter()
|
||||||
|
.cloned()
|
||||||
|
.map(|mut w| {
|
||||||
|
w.text_verts.clear();
|
||||||
|
w
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let p_bare = dir.join("ocs_385_no_text.pdf");
|
||||||
|
export_pdf(&stripped, &[], &[], 210.0, 297.0, 0.0, 0.0, 0, 1.0, None, &p_bare, None)
|
||||||
|
.expect("export without text");
|
||||||
|
let no_text = std::fs::read(&p_bare).expect("read pdf");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
with_text.len() > no_text.len(),
|
||||||
|
"text added no content to the PDF: {} !> {}",
|
||||||
|
with_text.len(),
|
||||||
|
no_text.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user