Print: Print and Print Preview warn about type that doesn't fit its frame

Area type and type on a path whose text doesn't fit print without the
text past the end of the frame. text.create, text.createInPath,
text.setText and text.setStyle report it in their replies, but
file.print and print.preview gave no warning, and the Print dialog's
Summary showed none.

Both commands now add a warning that counts the area type and type on
a path that prints and doesn't fit, and gives their ids (the first 20,
then how many more). It counts the objects that the fallback font
warning counts: visible objects on the layers that Print Layers picks,
on the paper of the artboards or tiles that print. The walk that finds
the fonts of the printed type also collects these ids. Type in the art
of symbols, patterns, brushes, opacity masks and placed documents is
not named.

Docs: docs/mcp.md (Print, Fitting area type), docs/target-app-parity.md
(Type).
This commit is contained in:
dotCooCoo
2026-10-16 15:27:54 +07:00
committed by bflatastic
parent dfe215ca30
commit adea2d307d
8 changed files with 204 additions and 34 deletions
+42 -21
View File
@@ -114,9 +114,12 @@ pub(super) fn used_fonts(d: &Document) -> BTreeSet<UsedFont> {
fonts
}
/// The fonts (family, style, version) of the type of `d` that prints on pages showing `areas`
/// (document space): visible type on `layers` (the layers that print) but not on a template layer
/// (of area type and type on a path that don't fit, the runs that their layout draws),
/// The type of `d` that prints on pages showing `areas` (document space): the fonts (family, style,
/// version) it is set in, and the ids of the area type and type on a path among the objects of the
/// document whose text doesn't fit (not those in the art of symbols, opacity masks, patterns,
/// brushes or placed documents).
/// The type that prints is visible type on `layers` (the layers that print) but not on a template
/// layer (of area type and type on a path that don't fit, the runs that their layout draws),
/// where what it paints ([`vectorcraft_render::painted_bounds`], its effects applied) meets an area
/// inside the clipping paths around it, the type in the opacity masks and patterns of the objects
/// that print and in the pieces their brushes draw, and the type of the symbols that their
@@ -125,27 +128,32 @@ pub(super) fn used_fonts(d: &Document) -> BTreeSet<UsedFont> {
/// fills or strokes of its own, are taken with it when what it paints meets an area, and so is the
/// art of a placed document (`d` as [`Document::with_placed_art`] gives it, for that art's
/// resources).
pub(super) fn printed_fonts(d: &Document, layers: &[Arc<Node>], areas: &[Rect]) -> BTreeSet<UsedFont> {
let mut walk = Printed { d, fonts: BTreeSet::new(), symbols: BTreeSet::new(), patterns: BTreeSet::new() };
pub(super) fn printed_type(d: &Document, layers: &[Arc<Node>], areas: &[Rect]) -> (BTreeSet<UsedFont>, BTreeSet<NodeId>) {
let mut walk = Printed { d, own: true, fonts: BTreeSet::new(), overflow: BTreeSet::new(), symbols: BTreeSet::new(), patterns: BTreeSet::new() };
for l in layers {
walk.node(l, Some(areas));
}
walk.fonts
(walk.fonts, walk.overflow)
}
/// The walk of [`printed_fonts`].
/// The walk of [`printed_type`].
struct Printed<'a> {
d: &'a Document,
/// Whether the walk is among the objects of the document, not in art drawn for one.
own: bool,
fonts: BTreeSet<UsedFont>,
/// The objects of the document whose text doesn't fit.
overflow: BTreeSet<NodeId>,
/// The symbols and patterns whose art was walked.
symbols: BTreeSet<String>,
patterns: BTreeSet<String>,
}
impl Printed<'_> {
/// Adds the fonts of `n` and its descendants that print: those whose bounds meet one of `areas`,
/// or with `None` (in the art of a symbol, opacity mask, pattern or placed document, in brush
/// pieces, or in a live object) all of them but those outside the clipping path of a clip group.
/// Adds the fonts of `n` and its descendants that print, and the ids of those that don't fit
/// while [`Printed::own`] is set: those whose bounds meet one of `areas`, or with `None` (in the
/// art of a symbol, opacity mask, pattern or placed document, in brush pieces, or in a live
/// object) all of them but those outside the clipping path of a clip group.
fn node(&mut self, n: &Node, areas: Option<&[Rect]>) {
if !n.visible || matches!(n.kind, NodeKind::Layer { template: true, .. }) {
return;
@@ -162,9 +170,9 @@ impl Printed<'_> {
}
// The art that its opacity mask, pattern paints and brushed strokes draw.
let mask = n.extras().mask.as_deref().filter(|m| !m.disabled);
let runs = match &n.kind {
let (runs, overflows) = match &n.kind {
NodeKind::Text(t) => drawn_runs(t),
_ => vec![],
_ => (vec![], false),
};
let items: Vec<&AppearanceItem> =
n.appearance.items.iter().filter(|i| i.visible() && !matches!(i, AppearanceItem::Stroke(s) if s.width <= 0.0)).collect();
@@ -180,7 +188,7 @@ impl Printed<'_> {
let brushed = vectorcraft_brush::has_brush(n);
if (mask.is_some() || !patterns.is_empty() || brushed) && areas.is_none_or(|a| !open || self.meets(n, a)) {
if let Some(m) = mask {
self.node(&m.art, None);
self.art(&m.art);
}
let d = self.d;
for name in patterns {
@@ -188,19 +196,22 @@ impl Printed<'_> {
&& let Some(p) = d.pattern(name)
{
for art in &p.art {
self.node(art, None);
self.art(art);
}
}
}
// The pieces its brushes draw along the path (art and pattern brushes leave type out).
if brushed {
for piece in vectorcraft_brush::node_pieces(d, n).unwrap_or_default() {
self.node(&piece, None);
self.art(&piece);
}
}
}
match &n.kind {
NodeKind::Text(_) => {
if overflows && self.own {
self.overflow.insert(n.id);
}
for r in runs {
self.fonts.insert((r.style.font_family.clone(), r.style.font_style.clone(), r.style.font_version.clone()));
// Inline graphics draw their symbols' art.
@@ -212,7 +223,7 @@ impl Printed<'_> {
NodeKind::SymbolInstance { symbol, .. } => self.symbol(symbol),
NodeKind::PlacedDocument(p) => {
for art in p.art(n) {
self.node(&art, None);
self.art(&art);
}
}
NodeKind::Layer { children, clip, .. } | NodeKind::Group { children, clip } if !moved => {
@@ -240,10 +251,19 @@ impl Printed<'_> {
if self.symbols.insert(name.to_string())
&& let Some(s) = d.symbols.iter().find(|s| s.name == name)
{
self.node(&s.art, None);
self.art(&s.art);
}
}
/// Adds the fonts of `art`, drawn for an object of the document but not one of its objects (the
/// art of a symbol, opacity mask, pattern or placed document, or a brush piece), with
/// [`Printed::node`]'s `None`.
fn art(&mut self, art: &Node) {
let own = std::mem::replace(&mut self.own, false);
self.node(art, None);
self.own = own;
}
/// Whether what `n` paints, its effects applied, meets one of `areas`.
fn meets(&self, n: &Node, areas: &[Rect]) -> bool {
vectorcraft_render::painted_bounds(self.d, n).is_some_and(|b| areas.iter().any(|r| b.intersect(*r).area() > 0.0 || r.contains_rect(b)))
@@ -256,14 +276,15 @@ impl Printed<'_> {
}
}
/// The runs of `t` that print: all of them, or of area type and type on a path that don't fit, the
/// runs that their layout draws glyphs or inline graphics of.
fn drawn_runs(t: &vectorcraft_doc::TextObject) -> Vec<&TextRun> {
/// The runs of `t` that print, and whether its text doesn't fit: all of them, or of area type and
/// type on a path that don't fit, the runs that their layout draws glyphs or inline graphics of.
fn drawn_runs(t: &vectorcraft_doc::TextObject) -> (Vec<&TextRun>, bool) {
let drawn: Option<BTreeSet<usize>> = (!matches!(t.kind, TextKind::Point))
.then(|| vectorcraft_text::layout(vectorcraft_text::FontDb::global(), t))
.filter(|lay| lay.overflow)
.map(|lay| lay.glyphs.iter().map(|g| g.run).chain(lay.inlines.iter().map(|i| i.run)).collect());
t.runs.iter().enumerate().filter(|(i, _)| drawn.as_ref().is_none_or(|d| d.contains(i))).map(|(_, r)| r).collect()
let runs = t.runs.iter().enumerate().filter(|(i, _)| drawn.as_ref().is_none_or(|d| d.contains(i))).map(|(_, r)| r).collect();
(runs, drawn.is_some())
}
/// What an export that draws type (as outlines, embedded fonts or pixels) says when `d` uses font
+12 -10
View File
@@ -31,7 +31,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Print Preview",
[],
None,
"{settings?: {…settings of print.setup} (over the document's)} → {pages (copies included), sheets: [{artboard (0-based; null with artboards ignored), tile? (1-based), ink? (separations), width, height (pt, the page), orientation, scale: [width %, height %], transform: [a, b, c, d, e, f] (document space → the page, pt, y down), area: [x0, y0, x1, y1] (the document region the page prints), trim: [x0, y0, x1, y1] (the artboard on the page)}] (one copy, in order), tiles: [{artboard, columns, rows, printed: [1-based tiles], tiles: [[x0, y0, x1, y1]…] (document space)}], inks: [{name, spot, print, frequency, angle}] (separations), warnings (art larger than the imageable area, type that prints in the fallback font, options not applied yet…), settings} without printing",
"{settings?: {…settings of print.setup} (over the document's)} → {pages (copies included), sheets: [{artboard (0-based; null with artboards ignored), tile? (1-based), ink? (separations), width, height (pt, the page), orientation, scale: [width %, height %], transform: [a, b, c, d, e, f] (document space → the page, pt, y down), area: [x0, y0, x1, y1] (the document region the page prints), trim: [x0, y0, x1, y1] (the artboard on the page)}] (one copy, in order), tiles: [{artboard, columns, rows, printed: [1-based tiles], tiles: [[x0, y0, x1, y1]…] (document space)}], inks: [{name, spot, print, frequency, angle}] (separations), warnings (art larger than the imageable area, type that prints in the fallback font, type that doesn't fit its frame, options not applied yet…), settings} without printing",
has_doc,
preview
),
@@ -92,7 +92,7 @@ fn preview(s: &mut Session, p: &Value) -> Result<Value> {
let set = settings(C, doc, p)?;
super::printadvanced::flattener(C, &set, &s.prefs.flattener_presets)?;
let mut pv = vectorcraft_pdf::preview(doc, &set).map_err(|e| pdf_error(C, e))?;
pv.warnings.extend(fonts_warning(doc, set.print_layers, &areas(&pv)));
pv.warnings.extend(type_warnings(doc, set.print_layers, &areas(&pv)));
let mut v = serde_json::to_value(pv).map_err(|e| EngineError::Other(e.to_string()))?;
v["settings"] = to_json(&set)?;
Ok(v)
@@ -118,14 +118,16 @@ fn on_page(area: Rect, to_page: Affine, (w, h): (f64, f64)) -> Option<Rect> {
(r.area() > 0.0).then_some(r)
}
/// The warning about type in `doc` that prints in the fallback font: on the layers `which` prints,
/// on pages that print `areas`.
fn fonts_warning(doc: &Document, which: PrintLayers, areas: &[Rect]) -> Option<String> {
/// The warnings about the type in `doc` that prints (on the layers `which` prints, on pages that
/// print `areas`): type in the fallback font, and type that doesn't fit its frame.
fn type_warnings(doc: &Document, which: PrintLayers, areas: &[Rect]) -> Vec<String> {
// Placed documents print their files' art, which names the resources this adds.
let doc = doc.with_placed_art();
let mut layers = doc.layers.clone();
vectorcraft_pdf::keep_layers(&mut layers, which);
super::fonts::substitution_warning_of(super::fonts::printed_fonts(&doc, &layers, areas))
let (fonts, overflow) = super::fonts::printed_type(&doc, &layers, areas);
let overflow: Vec<_> = overflow.into_iter().collect();
super::fonts::substitution_warning_of(fonts).into_iter().chain(super::typecmd::overflow_warning(&overflow)).collect()
}
const PRINT: &str = "file.print";
@@ -162,13 +164,13 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
// Print as Bitmap and the flattener preset (Advanced) render and flatten it first.
let prepared = super::printadvanced::prepare(PRINT, doc, &set, &s.prefs.flattener_presets, s.prefs.black_output == "rich")?;
let path = str_param(p, "path");
// Fonts are read from the type before raster effects and Print as Bitmap turn it into images.
// The type is read before raster effects and Print as Bitmap turn it into images.
let which = set.print_layers;
if !postscript {
let printed = prepared.as_ref().unwrap_or(doc);
let fonts = fonts_warning(&full, which, &areas(&vectorcraft_pdf::preview(printed, &set).map_err(|e| pdf_error(PRINT, e))?));
let typed = type_warnings(&full, which, &areas(&vectorcraft_pdf::preview(printed, &set).map_err(|e| pdf_error(PRINT, e))?));
let mut r = vectorcraft_pdf::print(printed, &PrintOptions { settings: set, ..Default::default() }).map_err(|e| pdf_error(PRINT, e))?;
r.warnings.extend(fonts);
r.warnings.extend(typed);
return write_or_return(path, &r.bytes, json!({ "pages": r.pages, "format": "pdf", "warnings": r.warnings }));
}
let level = match p.get("level").filter(|v| !v.is_null()) {
@@ -212,6 +214,6 @@ fn print(s: &mut Session, p: &Value) -> Result<Value> {
}
}
let areas: Vec<Rect> = plan.sheets.iter().filter_map(|sh| on_page(sh.area, sh.view * sh.place, sh.size)).collect();
warnings.extend(fonts_warning(&full, which, &areas));
warnings.extend(type_warnings(&full, which, &areas));
write_or_return(path, &out.bytes, json!({ "pages": plan.order.len(), "format": "postscript", "warnings": warnings }))
}
+17
View File
@@ -269,6 +269,23 @@ pub(crate) fn with_overflow(s: &Session, ids: &[NodeId], mut result: Value) -> V
result
}
/// The warning that Print gives for `ids`, area type and type on a path whose text doesn't fit
/// (as [`with_overflow`] finds it): the text past the end of their frames doesn't print. It names
/// the first 20 ids and counts the rest; `None` when `ids` is empty.
pub(crate) fn overflow_warning(ids: &[NodeId]) -> Option<String> {
const NAMED: usize = 20;
let (named, rest) = ids.split_at(ids.len().min(NAMED));
let mut list = named.iter().map(|i| i.0.to_string()).collect::<Vec<_>>().join(", ");
if !rest.is_empty() {
list = format!("{list} and {} more", rest.len());
}
match ids {
[] => None,
[_] => Some(format!("1 text object doesn't fit its frame: the text past the end of the frame doesn't print (id {list})")),
_ => Some(format!("{} text objects don't fit their frames: the text past the end of each frame doesn't print (ids {list})", ids.len())),
}
}
/// Text objects among `ids` and their descendants.
fn text_ids(d: &Document, ids: &[NodeId]) -> Vec<NodeId> {
let mut out = vec![];