mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 16:33:22 -06:00
Export: PDF, EPS, SVG, EMF, WMF and image exports warn about type that doesn't fit its frame
Print and Print Preview name the area type and type on a path that prints without the text past the end of its frame (#2308). The exports that draw type gave no such warning: a PDF, EPS, SVG, EMF, WMF or raster export of a frame too small for its text answered with no warning about it. These exports now add the same warning, with "isn't drawn in the export" in place of "doesn't print". Each export counts the visible objects that it draws. Those are the objects on the pages or artboards it writes, or all of them in an EPS of the visible art and in an SVG without chosen artboards. A PDF leaves out layers that don't print unless includeNonPrinting or createLayers is set. The objects are found with the walk that fonts::printed_type runs for Print, and typecmd.rs builds the message with the code that builds Print's. A .ai file keeps the whole text in its editing data and gives no warning. Docs: docs/mcp.md (Fitting area type), docs/target-app-parity.md.
This commit is contained in:
@@ -385,7 +385,7 @@ pub fn encode(doc: &Document, format: &str, p: &Value) -> Result<Vec<u8>> {
|
||||
|
||||
/// Like [`encode`], also returning the encoder's warnings (PDF and SVG: options not applied yet,
|
||||
/// features approximated or left out; every format that draws type: fonts written in the fallback
|
||||
/// font).
|
||||
/// font; PDF, EPS, SVG, EMF, WMF and the raster formats: type that doesn't fit its frame).
|
||||
pub fn encode_with_warnings(doc: &Document, format: &str, p: &Value) -> Result<(Vec<u8>, Vec<String>)> {
|
||||
let f = super::writable(C, Some(format), None)?;
|
||||
encode_all(doc, f.id, p)?.single(f)
|
||||
@@ -529,6 +529,8 @@ fn encode_files(doc: &Document, f: &Format, p: &Value) -> Result<Encoded> {
|
||||
if enc.files.is_empty() {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
}
|
||||
let regions: Vec<Rect> = enc.files.iter().filter_map(|(b, _)| b.and_then(|b| doc.artboards.get(b))).map(|a| a.rect).collect();
|
||||
enc.warnings.extend(crate::cmd::fonts::export_overflow_warning(doc, &doc.layers, Some(®ions)));
|
||||
if format == RasterFormat::WebP && o.lossless == Some(false) {
|
||||
enc.warnings.push("lossy WebP isn't available yet: the file is lossless (exact, but larger)".into());
|
||||
}
|
||||
|
||||
@@ -270,6 +270,7 @@ pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
if pages.is_empty() {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
}
|
||||
let regions: Vec<Rect> = pages.iter().map(|(_, region, _)| *region).collect();
|
||||
let transparent = doc.layers.iter().any(|l| l.shows_transparency());
|
||||
let flat = flatten_document(doc, &set.flatten)?;
|
||||
let drawn = flat.as_ref().unwrap_or(doc);
|
||||
@@ -302,6 +303,7 @@ pub(super) fn encode(doc: &Document, p: &Value) -> Result<Encoded> {
|
||||
}
|
||||
}
|
||||
}
|
||||
enc.warnings.extend(crate::cmd::fonts::export_overflow_warning(doc, &doc.layers, Some(®ions)));
|
||||
Ok(enc)
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ pub(super) fn encode(doc: &Document, p: &Value, use_artboards: Option<bool>, kin
|
||||
if enc.files.is_empty() {
|
||||
return Err(bad(C, "the document has no artboard"));
|
||||
}
|
||||
let regions: Vec<vectorcraft_geom::Rect> = enc.files.iter().filter_map(|(b, _)| b.and_then(|b| doc.artboards.get(b))).map(|a| a.rect).collect();
|
||||
enc.warnings.extend(crate::cmd::fonts::export_overflow_warning(doc, &doc.layers, Some(®ions)));
|
||||
Ok(enc)
|
||||
}
|
||||
|
||||
|
||||
@@ -254,9 +254,21 @@ pub fn encode(cmd: &str, doc: &Document, p: &Value) -> Result<(Vec<u8>, Vec<Stri
|
||||
let (full, mut warnings) = crate::cmd::place::document::full_documents(doc);
|
||||
let (bytes, drawn_warnings) = encode_carrying(cmd, &full, p, || Ok(vectorcraft_format::save(doc, false)))?;
|
||||
warnings.extend(drawn_warnings);
|
||||
warnings.extend(overflow_warning(cmd, &full, p)?);
|
||||
Ok((bytes, warnings))
|
||||
}
|
||||
|
||||
/// The warning about the type that the pages of a PDF of `doc` with the options in `p` draw and
|
||||
/// whose text doesn't fit (see [`crate::cmd::fonts::export_overflow_warning`]): on the pages
|
||||
/// chosen, leaving out the layers that don't print unless the PDF writes them.
|
||||
fn overflow_warning(cmd: &str, doc: &Document, p: &Value) -> Result<Option<String>> {
|
||||
let opts = options(cmd, doc, p)?;
|
||||
let areas = vectorcraft_pdf::page_areas(doc, &opts).map_err(|e| pdf_error(cmd, e))?;
|
||||
let set = &opts.settings;
|
||||
let printed = if set.include_non_printing || set.create_layers { None } else { without_non_printing(&doc.layers) };
|
||||
Ok(crate::cmd::fonts::export_overflow_warning(doc, printed.as_deref().unwrap_or(&doc.layers), Some(&areas)))
|
||||
}
|
||||
|
||||
/// [`encode`] the pages of `doc`, carrying `native()` as the editing data (a `.ai` file: the
|
||||
/// native document with its save options, whose pages may be left blank).
|
||||
pub(super) fn encode_carrying(cmd: &str, doc: &Document, p: &Value, native: impl FnOnce() -> Result<Vec<u8>>) -> Result<(Vec<u8>, Vec<String>)> {
|
||||
|
||||
@@ -157,6 +157,10 @@ pub(super) fn encode(doc: &Document, p: &Value, compressed: bool) -> Result<Enco
|
||||
// that preparation out of the native attachment so missing links remain relinkable.
|
||||
let (full, warnings) = crate::cmd::place::document::full_documents(doc);
|
||||
let doc = &*full;
|
||||
// A chosen artboard's file holds the art over it; otherwise the file holds all of it.
|
||||
let regions: Option<Vec<vectorcraft_geom::Rect>> =
|
||||
chosen.then(|| boards.iter().flatten().filter_map(|b| doc.artboards.get(*b)).map(|a| a.rect).collect());
|
||||
let overflow = crate::cmd::fonts::export_overflow_warning(doc, &doc.layers, regions.as_deref());
|
||||
// SVG has no filters for the Photoshop-style effects: their objects go in as images.
|
||||
let flat = crate::cmd::rasterfx::flatten_pixel_effects(doc);
|
||||
let doc = flat.as_ref().unwrap_or(doc);
|
||||
@@ -184,5 +188,6 @@ pub(super) fn encode(doc: &Document, p: &Value, compressed: bool) -> Result<Enco
|
||||
{
|
||||
enc.warnings.push(w);
|
||||
}
|
||||
enc.warnings.extend(overflow);
|
||||
Ok(enc)
|
||||
}
|
||||
|
||||
@@ -136,6 +136,19 @@ pub(super) fn printed_type(d: &Document, layers: &[Arc<Node>], areas: &[Rect]) -
|
||||
(walk.fonts, walk.overflow)
|
||||
}
|
||||
|
||||
/// The warning of an export that draws `layers` of `d` over `areas` (document space; `None`: the
|
||||
/// whole of them) about the area type and type on a path that it draws and whose text doesn't fit,
|
||||
/// found as [`printed_type`] finds them for Print: visible objects of the document that aren't on
|
||||
/// a template layer.
|
||||
pub(super) fn export_overflow_warning(d: &Document, layers: &[Arc<Node>], areas: Option<&[Rect]>) -> Option<String> {
|
||||
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, areas);
|
||||
}
|
||||
let overflow: Vec<NodeId> = walk.overflow.into_iter().collect();
|
||||
super::typecmd::export_overflow_warning(&overflow)
|
||||
}
|
||||
|
||||
/// The walk of [`printed_type`].
|
||||
struct Printed<'a> {
|
||||
d: &'a Document,
|
||||
|
||||
@@ -315,6 +315,17 @@ pub(crate) fn with_bounds(s: &Session, id: Option<NodeId>, mut result: Value) ->
|
||||
/// (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> {
|
||||
overflow_note(ids, "doesn't print")
|
||||
}
|
||||
|
||||
/// [`overflow_warning`] as an export that draws type gives it: the text past the end of the frames
|
||||
/// isn't drawn in the export.
|
||||
pub(crate) fn export_overflow_warning(ids: &[NodeId]) -> Option<String> {
|
||||
overflow_note(ids, "isn't drawn in the export")
|
||||
}
|
||||
|
||||
/// [`overflow_warning`], saying that the text past the end of the frames `left_out`.
|
||||
fn overflow_note(ids: &[NodeId], left_out: &str) -> 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(", ");
|
||||
@@ -323,8 +334,8 @@ pub(crate) fn overflow_warning(ids: &[NodeId]) -> Option<String> {
|
||||
}
|
||||
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())),
|
||||
[_] => Some(format!("1 text object doesn't fit its frame: the text past the end of the frame {left_out} (id {list})")),
|
||||
_ => Some(format!("{} text objects don't fit their frames: the text past the end of each frame {left_out} (ids {list})", ids.len())),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -483,3 +483,71 @@ fn making_and_restyling_type_reports_its_overflow() {
|
||||
let inside = s.execute("text.createInPath", &json!({"path": frame["id"], "mode": "area", "text": STORY, "size": 12, "fit": "none"})).unwrap();
|
||||
assert_eq!(inside["overflow"], json!([inside["id"]]), "{inside}");
|
||||
}
|
||||
|
||||
/// The formats whose exports draw type.
|
||||
const DRAWING_FORMATS: [&str; 8] = ["pdf", "eps", "svg", "png", "jpg", "tiff", "emf", "wmf"];
|
||||
|
||||
/// The warnings of `document.export {format, …options}` about type that doesn't fit its frame.
|
||||
fn export_overflow(s: &mut Session, format: &str, options: Value) -> Vec<String> {
|
||||
let mut p = json!({ "format": format });
|
||||
if let (Some(p), Some(o)) = (p.as_object_mut(), options.as_object()) {
|
||||
p.extend(o.clone());
|
||||
}
|
||||
let v = s.execute("document.export", &p).unwrap();
|
||||
let over = |w: &&str| w.contains("fit its frame") || w.contains("fit their frames");
|
||||
v["warnings"].as_array().unwrap().iter().filter_map(Value::as_str).filter(over).map(str::to_string).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_that_draw_type_name_the_type_that_does_not_fit() {
|
||||
let mut s = session();
|
||||
let id = area(&mut s, 100.0, 20.0, "Ab\nCd\nEf\nGh", json!({}));
|
||||
assert!(overflows(&s, id));
|
||||
let one = format!("1 text object doesn't fit its frame: the text past the end of the frame isn't drawn in the export (id {})", id.0);
|
||||
for format in DRAWING_FORMATS {
|
||||
assert_eq!(export_overflow(&mut s, format, json!({})), [one.as_str()], "{format}");
|
||||
}
|
||||
// A .ai file carries the whole text in its editing data.
|
||||
assert_eq!(export_overflow(&mut s, "ai", json!({})), Vec::<String>::new());
|
||||
// A frame that holds its text, or a hidden object, leaves nothing out.
|
||||
s.execute("text.areaOptions", &json!({"ids": [id.0], "height": 150})).unwrap();
|
||||
for format in DRAWING_FORMATS {
|
||||
assert_eq!(export_overflow(&mut s, format, json!({})), Vec::<String>::new(), "{format}");
|
||||
}
|
||||
s.execute("text.areaOptions", &json!({"ids": [id.0], "height": 20})).unwrap();
|
||||
s.execute("object.setProps", &json!({"ids": [id.0], "visible": false})).unwrap();
|
||||
// The type is hidden, so a rectangle gives the EPS of the visible art something to export.
|
||||
s.execute("shape.rectangle", &json!({"x": 300, "y": 300, "width": 50, "height": 50})).unwrap();
|
||||
for format in DRAWING_FORMATS {
|
||||
assert_eq!(export_overflow(&mut s, format, json!({})), Vec::<String>::new(), "{format}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_name_only_the_type_on_the_artboards_and_layers_they_draw() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 300, "height": 200, "artboards": 2})).unwrap();
|
||||
let r = s.doc().unwrap().doc.artboards[1].rect;
|
||||
let at = json!({"x": r.x0 + 10.0, "y": r.y0 + 10.0, "text": "Ab\nCd\nEf\nGh", "size": 12, "area": {"width": 100, "height": 20}});
|
||||
let v = s.execute("text.create", &at).unwrap();
|
||||
assert_eq!(v["overflow"], json!([v["id"]]), "{v}");
|
||||
for (format, first, second) in [
|
||||
("pdf", json!({"range": "1"}), json!({"range": "2"})),
|
||||
("eps", json!({"useArtboards": true, "range": "1"}), json!({"useArtboards": true, "range": "2"})),
|
||||
("svg", json!({"range": "1"}), json!({"range": "2"})),
|
||||
("png", json!({"artboard": 0}), json!({"artboard": 1})),
|
||||
("emf", json!({"artboard": 0}), json!({"artboard": 1})),
|
||||
] {
|
||||
assert_eq!(export_overflow(&mut s, format, first), Vec::<String>::new(), "{format}");
|
||||
assert_eq!(export_overflow(&mut s, format, second).len(), 1, "{format}");
|
||||
}
|
||||
// An EPS of the visible art and an SVG of the whole document hold it too.
|
||||
assert_eq!(export_overflow(&mut s, "eps", json!({})).len(), 1);
|
||||
assert_eq!(export_overflow(&mut s, "svg", json!({})).len(), 1);
|
||||
// On a layer that doesn't print: a PDF leaves it out unless asked to keep it; a PNG draws it.
|
||||
let layer = s.doc().unwrap().doc.layers[0].id;
|
||||
s.execute("layer.setProps", &json!({"id": layer.0, "printable": false})).unwrap();
|
||||
assert_eq!(export_overflow(&mut s, "pdf", json!({"range": "2"})), Vec::<String>::new());
|
||||
assert_eq!(export_overflow(&mut s, "pdf", json!({"range": "2", "includeNonPrinting": true})).len(), 1);
|
||||
assert_eq!(export_overflow(&mut s, "png", json!({"artboard": 1})).len(), 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user