diff --git a/crates/engine/src/cmd/fileio/load.rs b/crates/engine/src/cmd/fileio/load.rs index aaaf198fa..147337fc8 100644 --- a/crates/engine/src/cmd/fileio/load.rs +++ b/crates/engine/src/cmd/fileio/load.rs @@ -168,6 +168,12 @@ fn through_editing_data(bytes: &[u8], opts: &LoadOptions, doc: Document, warning if !read { return (doc, warnings); } + // The layers' text objects hold type and the stroked paths around it, which can include stroked + // glyphs the PDF part kept as outlines: without any such glyph in the document, the warnings on + // outlines are dropped. + if !vectorcraft_pdf::holds_outlines(&doc) { + warnings.retain(|w| !vectorcraft_pdf::is_outlines_note(w)); + } // The layers hold the art that the PDF part leaves out or, in a PDF, that the import clipped // to the page. warnings.retain(|w| w != vectorcraft_pdf::OFF_ARTBOARD_NOTE && w != vectorcraft_pdf::PAST_PAGE_NOTE); diff --git a/crates/engine/src/cmd/fileio/tests_aiimport.rs b/crates/engine/src/cmd/fileio/tests_aiimport.rs index 80ec9d7f3..e24bdf10f 100644 --- a/crates/engine/src/cmd/fileio/tests_aiimport.rs +++ b/crates/engine/src/cmd/fileio/tests_aiimport.rs @@ -303,6 +303,144 @@ fn type_asked_for_as_outlines_opens_a_file_whose_type_shows_from_its_pdf_part() assert_ne!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}"); } +/// `data` as ASCII85, up to the `~>`. +fn ascii85(data: &[u8]) -> String { + let mut out = String::new(); + for chunk in data.chunks(4) { + let mut x = chunk.iter().enumerate().fold(0u32, |v, (i, b)| v | u32::from(*b) << (24 - 8 * i)); + let mut d = [0u8; 5]; + for c in d.iter_mut().rev() { + *c = (x % 85) as u8 + b'!'; + x /= 85; + } + out.extend(d.iter().take(chunk.len() + 1).map(|c| char::from(*c))); + } + out.push_str("~>"); + out +} + +/// Page content drawing one glyph and then `rest` in 10 pt on the baseline at `y`, from x = 14, in +/// rendering mode `mode` (0 fills the type, 1 strokes it). The glyph is code 0x41 of the font `F2`, +/// standing for the "fi" of the story, and `F2` isn't installed and its `ToUnicode` map gives that +/// glyph U+FFFD, so that the PDF part keeps it as outlines; `rest` is in Helvetica (`F1`). +fn half_outlined_line(y: f64, rest: &str, mode: u8) -> String { + format!("{mode} Tr BT /F2 10 Tf 14 {y} Td (A) Tj ET BT /F1 10 Tf 19 {y} Td ({rest}) Tj ET ") +} + +/// A `.ai` whose page draws a gray square and `type_content` (see [`half_outlined_line`]), and whose +/// layer `Art` has the square and text objects: the first of story 0, which `story` gives in the +/// text document (a line starting at (14, 65)), then `more` of a story the text document doesn't +/// have. Without `story` the file has no text document. +fn half_outlined_ai(story: Option<&str>, more: usize, type_content: &str) -> Vec { + let text = |story: u32| format!("/AI11Text :\n0 /FreeUndo ,\n0 /FrameIndex ,\n{story} /StoryIndex ,\n;\n"); + let texts: String = std::iter::once(text(0)).chain((0..more).map(|_| text(99))).collect(); + let mut body = + format!("%AI3_TemplateBox: 100 50 100 50\n{}", ai::layer("Art", &format!("0.5 g\n{}f\n{texts}", ai::rect(120.0, 20.0, 160.0, 60.0)))); + if let Some(story) = story { + // The story's one line starts at the canvas center (8191.5, the template box's center) moved + // to (14, 65): its y runs down the page. + let n = story.chars().count(); + let document = format!( + "/0 << /1 << /0 [ << /0 << /0 << /0 (Helvetica) >> >> >> ] >> /8 << /0 [ << /0 << /0 [ 0 0 ] /2 << /2 [ 1 0 0 1 0 0 ] >> >> >> ] >> >>\n\ + /1 << /1 [ << /0 << /0 ({story}\r) /5 << /0 [ << /0 << /0 << /0 () /5 << /0 0 >> /6 0 >> >> /1 {} >> ] >> \ + /6 << /0 [ << /0 << /0 << /0 () /5 0 /6 << /0 0 /1 10.0 /53 << /99 /CAITextPaint /0 << /0 2 /1 [ 1.0 0.0 0.0 0.0 1.0 ] >> >> >> >> >> /1 {} >> ] >> >> \ + /1 << /0 [ << /0 0 >> ] /2 [ << /99 /PC /6 [ << /99 /F /0 << /0 [ 8105.5 8176.5 ] >> /6 [ << /99 /R /6 [ << /99 /R /6 [ << /99 /L /6 [ \ + << /99 /S /0 << /0 [ 0.0 0.0 ] >> /15 << /0 {} >> >> ] >> ] >> ] >> ] >> ] >> ] >> >> ] /2 << /1 12.0 >> >>\n", + n + 1, + n + 1, + n + 1 + ); + let lines: Vec = ascii85(document.as_bytes()).as_bytes().chunks(60).map(|c| format!("%{}", String::from_utf8_lossy(c))).collect(); + body.push_str(&format!("%AI11_BeginTextDocument\n/AI11TextDocument : /ASCII85Decode ,\n{}\n%AI11_EndTextDocument\n", lines.join("\n"))); + } + let data = ai::editing_data(200.0, 100.0, &body); + let content = format!("0.5 g 120 20 40 40 re f 0.2 g 0.2 G {type_content}"); + let page = PdfPage { + resources: format!( + "/Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >> \ + /F2 << /Type /Font /Subtype /Type1 /BaseFont /VectorCraftTestUnnamed /ToUnicode {} 0 R >> >>", + first_extra(1) + 1 + ), + entries: format!("/PieceInfo << /Illustrator << /Private << /AIPrivateData1 {} 0 R /NumBlock 1 >> >> >>", first_extra(1)), + ..PdfPage::new(200.0, 100.0, &content) + }; + let cmap = "begincmap\n1 begincodespacerange\n<00> \nendcodespacerange\n1 beginbfchar\n<41> \nendbfchar\nendcmap\n"; + pdf_with( + &[page], + &[&format!("<< /Length {} >>\nstream\n{data}\nendstream", data.len()), &format!("<< /Length {} >>\nstream\n{cmap}endstream", cmap.len())], + None, + ) +} + +fn outlines_notes(r: &Value) -> Vec { + let notes = r["warnings"].as_array().unwrap().iter().filter_map(Value::as_str); + notes.filter(|w| vectorcraft_pdf::is_outlines_note(w)).map(String::from).collect() +} + +/// The notes the PDF part of [`half_outlined_ai`] reads with: its glyph of U+FFFD, kept as outlines. +fn pdf_part_outlines_notes() -> [String; 2] { + [format!("text in Vector Craft Test Unnamed {}", vectorcraft_pdf::UNNAMED_CHARACTERS_NOTE), vectorcraft_pdf::OUTLINED_TEXT_NOTE.into()] +} + +/// The note on text objects that hold the page's type without some of its characters, for `n` of +/// them. +fn without_outlines_note(n: usize) -> Value { + json!(format!("{n} text objects hold their type as the page draws it, without the characters the page has only as outlines")) +} + +/// The texts in the first layer. +fn texts(s: &Session) -> Vec { + let art = s.doc().unwrap().doc.layers[0].children().unwrap(); + art.iter().filter_map(|n| if let NodeKind::Text(t) = &n.kind { Some(t.plain_text()) } else { None }).collect() +} + +/// The note that glyphs were kept as outlines is dropped when the type comes from the editing data's +/// text document, and stays with the PDF part alone. +#[test] +fn type_from_the_text_document_opens_without_the_pdf_parts_note_on_outlines() { + let file = half_outlined_ai(Some("first draft"), 0, &half_outlined_line(65.0, "rst draft", 0)); + let mut s = Session::new(); + let r = open(&mut s, "draft.ai", &file, json!({})).unwrap(); + assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}"); + assert_eq!(texts(&s), ["first draft"], "{r}"); + assert_eq!(outlines_notes(&r), Vec::::new(), "{r}"); + assert!(!r["warnings"].as_array().unwrap().contains(&without_outlines_note(1)), "{r}"); + let r = open(&mut s, "draft.ai", &file, json!({"editingData": false})).unwrap(); + assert_eq!(outlines_notes(&r), pdf_part_outlines_notes(), "{r}"); + // A second text object that takes the page's type, away from the glyph: no note on it. + let page = half_outlined_line(65.0, "rst draft", 0) + "BT /F1 10 Tf 14 30 Td (notes) Tj ET"; + let r = open(&mut s, "notes.ai", &half_outlined_ai(Some("first draft"), 1, &page), json!({})).unwrap(); + assert_eq!(texts(&s), ["first draft", "notes"], "{r}"); + assert_eq!(outlines_notes(&r), Vec::::new(), "{r}"); + assert!(!r["warnings"].as_array().unwrap().contains(&without_outlines_note(1)), "{r}"); +} + +/// A text object that takes the page's type lacks the glyphs next to it that the page kept as +/// outlines: a note counts such objects, in place of the PDF part's notes. Stroked glyphs that +/// come in with the stroked type keep the PDF part's notes. +#[test] +fn type_from_the_page_opens_with_a_note_on_the_characters_it_has_only_as_outlines() { + let mut s = Session::new(); + let r = open(&mut s, "draft.ai", &half_outlined_ai(None, 0, &half_outlined_line(65.0, "rst draft", 0)), json!({})).unwrap(); + assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}"); + assert_eq!(texts(&s), ["rst draft"], "{r}"); + assert!(!vectorcraft_pdf::holds_outlines(&s.doc().unwrap().doc), "{r}"); + assert_eq!(outlines_notes(&r), Vec::::new(), "{r}"); + assert!(r["warnings"].as_array().unwrap().contains(&without_outlines_note(1)), "{r}"); + // Stroked: the glyph comes in with the type. + let r = open(&mut s, "draft.ai", &half_outlined_ai(None, 0, &half_outlined_line(65.0, "rst draft", 1)), json!({})).unwrap(); + assert_eq!(names(&s.doc().unwrap().doc.layers), ["Art"], "{r}"); + assert!(vectorcraft_pdf::holds_outlines(&s.doc().unwrap().doc), "{r}"); + assert_eq!(outlines_notes(&r), pdf_part_outlines_notes(), "{r}"); + assert!(!r["warnings"].to_string().contains("without the characters"), "{r}"); + // A stroked line whose glyph comes in, and a filled one whose glyph doesn't. + let page = half_outlined_line(65.0, "rst draft", 1) + &half_outlined_line(30.0, "nd more", 0); + let r = open(&mut s, "both.ai", &half_outlined_ai(None, 1, &page), json!({})).unwrap(); + assert_eq!(texts(&s), ["rst draft", "nd more"], "{r}"); + assert_eq!(outlines_notes(&r), pdf_part_outlines_notes(), "{r}"); + assert!(r["warnings"].as_array().unwrap().contains(&without_outlines_note(1)), "{r}"); +} + /// A `.ai` saved in units of 10 points: its PDF part's page is 200 × 100 with `/UserUnit 10`, and /// its editing data has the artboard and a red rectangle in the same units. It opens at 2000 × 1000 /// pt, from its editing data and from its PDF part alone, with the rectangle ten times as large, diff --git a/crates/eps/src/import/native.rs b/crates/eps/src/import/native.rs index ec5138e21..19269a165 100644 --- a/crates/eps/src/import/native.rs +++ b/crates/eps/src/import/native.rs @@ -58,7 +58,8 @@ const MAX_SLOTS: usize = 5000; const MAX_PIECES: usize = 2000; /// Most bytes of text in the text objects of the layers (a story named twice counts twice). const MAX_TEXT: usize = 16 << 20; -/// Most pairs of a text object and a piece of the page's type compared. +/// Most pairs of a text object and a piece of the page's type compared, and of a glyph the page +/// kept as outlines and a node of the page's type. const MAX_PAIRS: usize = 4_000_000; /// `names` for a note: the first few, and how many more. @@ -978,6 +979,8 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr }); let visible = small.as_ref().or(visible); let (mut shown, mut hidden) = (vec![], vec![]); + // The glyphs the page kept as outlines that no text object of it takes in, one by one. + let mut lone: Vec>> = vec![]; let mut frame = None; if let Some(visible) = visible { let art_box = match page { @@ -998,13 +1001,19 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr text_objects(c, l.visible, &mut shown, &mut hidden); } } + let taken: BTreeSet = shown.iter().chain(&hidden).flatten().map(|n| n.id).collect(); + visible.walk(|n| { + if vectorcraft_pdf::is_glyph_outlines(n) && !taken.contains(&n.id) { + lone.push(vec![Arc::new(n.clone())]); + } + }); // A `.ai` read as its PDF part has a page for each artboard, in a row: the type on each // goes where the layers have that artboard. let pages: Vec = visible.artboards.iter().map(|a| a.rect).collect(); let same = |(a, p): (&vectorcraft_doc::Artboard, &Rect)| (a.rect.width() - p.width()).abs() <= 0.5 && (a.rect.height() - p.height()).abs() <= 0.5; let boards = (pages.len() == doc.artboards.len() && doc.artboards.iter().zip(&pages).all(same)).then_some(&doc.artboards); - for object in shown.iter_mut().chain(hidden.iter_mut()) { + for object in shown.iter_mut().chain(hidden.iter_mut()).chain(lone.iter_mut()) { // The page with most of it, the first for what is on none. let on = extent(object).and_then(|r| { let inside = |p: &Rect| p.intersect(r).area().max(0.0); @@ -1066,6 +1075,15 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr let template = template.map(|t| ((t[0] + t[2]) / 2.0, (t[1] + t[3]) / 2.0)); let mut notes = warnings; let content = plan(&infos, &stories, template, to_doc, [&shown, &hidden], &mut notes); + // The glyphs left out with the rest of the page's art, and how many text objects take type of + // the page right next to them (not counted past `MAX_PAIRS` comparisons). + let compared = lone.len().saturating_mul(shown.iter().chain(&hidden).map(Vec::len).sum()); + let missing: Vec = if compared > MAX_PAIRS { vec![] } else { lone.iter().filter_map(|g| extent(g)).collect() }; + let next_to_missing = |n: &Arc| { + matches!(n.kind, NodeKind::Text(_)) + && n.visual_bounds().is_some_and(|b| missing.iter().any(|r| !b.inflate(OUTLINE_REACH, OUTLINE_REACH).intersect(*r).is_zero_area())) + }; + let short = content.iter().filter(|c| matches!(c, Content::Page(p) | Content::Made(_, _, _, p) if p.iter().any(next_to_missing))).count(); let mut empty = 0; let mut layers = std::mem::take(&mut doc.layers); let mut made = vec![]; @@ -1081,6 +1099,9 @@ fn build(data: &[u8], visible: Option<&Document>, page: Page, outlined: bool, tr if empty > 0 { notes.push(format!("{empty} {}", if frame.is_some() { TEXT_LEFT_OUT } else { TYPE_LEFT_OUT })); } + if short > 0 { + notes.push(format!("{short} {PAGE_TYPE_WITHOUT_OUTLINES}")); + } if !hidden_unread.is_empty() { notes.push(format!("{ART_LEFT_OUT} ({}), left out of them", few(hidden_unread))); } @@ -1136,6 +1157,9 @@ const MADE: &str = "\u{1}made"; const MAX_WEIGHED: usize = 12; /// The text objects the file keeps that the page doesn't draw. const UNSEEN_TEXT: &str = "text objects that the file keeps but its page doesn't draw (what is left of type turned to outlines) are left out"; +/// The text objects that hold the page's type next to glyphs the page kept as outlines, which are +/// left out. +const PAGE_TYPE_WITHOUT_OUTLINES: &str = "text objects hold their type as the page draws it, without the characters the page has only as outlines"; /// How much more of the page (0 to 1) the layers may differ without a text object and still look no worse. const UNSEEN_MARGIN: f64 = 5e-5; diff --git a/crates/pdf/src/import.rs b/crates/pdf/src/import.rs index 9d9b8fe1f..73ac0811f 100644 --- a/crates/pdf/src/import.rs +++ b/crates/pdf/src/import.rs @@ -147,6 +147,32 @@ pub const OFF_ARTBOARD_NOTE: &str = /// Warning: art reaching past the page box was clipped to it. pub const PAST_PAGE_NOTE: &str = "art reaching past the page was clipped to it, as PDF viewers show it"; +/// Warning: glyphs were kept as outlines rather than read as type. +pub const OUTLINED_TEXT_NOTE: &str = "text was converted to outlines"; +/// Warning: glyphs that differ from the installed font of their name were kept as outlines. +pub const UNMATCHED_GLYPHS_NOTE: &str = "text whose glyphs differ from the installed font of the same name was kept as outlines"; +/// The end of the warning that glyphs whose characters the file doesn't name were kept as +/// outlines, after "text in" and the font's name. +pub const UNNAMED_CHARACTERS_NOTE: &str = "whose characters the file doesn't name (U+FFFD) was kept as outlines"; + +/// Is `note` one of the warnings that glyphs were kept as outlines ([`OUTLINED_TEXT_NOTE`], +/// [`UNMATCHED_GLYPHS_NOTE`], [`UNNAMED_CHARACTERS_NOTE`])? +pub fn is_outlines_note(note: &str) -> bool { + note == OUTLINED_TEXT_NOTE || note == UNMATCHED_GLYPHS_NOTE || note.starts_with("text in ") && note.ends_with(UNNAMED_CHARACTERS_NOTE) +} + +/// Is `n` glyphs kept as outlines (a path the import names ``)? +pub fn is_glyph_outlines(n: &Node) -> bool { + matches!(n.kind, NodeKind::Path { .. }) && n.name.as_deref() == Some(TEXT_OUTLINES) +} + +/// Does `doc` hold glyphs kept as outlines ([`is_glyph_outlines`])? +pub fn holds_outlines(doc: &Document) -> bool { + let mut any = false; + doc.walk(|n| any |= is_glyph_outlines(n)); + any +} + /// Import a PDF, returning the document plus warnings about content that was approximated or skipped. pub fn import_with_report(bytes: &[u8], opts: &ImportOptions) -> Result { // The interpreter draws forms, patterns, glyphs and masks inside one another by recursion, in @@ -1655,7 +1681,7 @@ impl<'p> Builder<'p> { None => { if o.as_unicode().is_some_and(|u| replacement(&u)) { let name = self.font_name(key, o).family; - self.warn(&format!("text in {name} whose characters the file doesn't name (U+FFFD) was kept as outlines")); + self.warn(&format!("text in {name} {UNNAMED_CHARACTERS_NOTE}")); } return false; } @@ -1689,7 +1715,7 @@ impl<'p> Builder<'p> { } }; let Some(f) = decided else { - self.warn("text whose glyphs differ from the installed font of the same name was kept as outlines"); + self.warn(UNMATCHED_GLYPHS_NOTE); return false; }; info = FontInfo { family: f.family.clone(), style: f.style.clone(), face: Some(f) }; @@ -1957,7 +1983,7 @@ impl<'a> Device<'a> for Builder<'_> { if let Some(r) = &mut self.glyphs { r.path.extend(bp.iter()); } - self.warn("text was converted to outlines"); + self.warn(OUTLINED_TEXT_NOTE); } Glyph::Type3(t3) => { if self.nested >= MAX_NESTED { diff --git a/crates/pdf/src/lib.rs b/crates/pdf/src/lib.rs index b3be1b3dd..e6dddd821 100644 --- a/crates/pdf/src/lib.rs +++ b/crates/pdf/src/lib.rs @@ -73,7 +73,10 @@ pub use editing::{EDITING_FILE, Editing, LEGACY_EDITING_FILE, editing, editing_w pub use encrypt::Encryption; pub use export::{export, export_with_report, page_areas}; pub use file_info::{FileInfo, file_info}; -pub use import::{OFF_ARTBOARD_NOTE, PAST_PAGE_NOTE, import, import_with_report}; +pub use import::{ + OFF_ARTBOARD_NOTE, OUTLINED_TEXT_NOTE, PAST_PAGE_NOTE, UNMATCHED_GLYPHS_NOTE, UNNAMED_CHARACTERS_NOTE, holds_outlines, import, + import_with_report, is_glyph_outlines, is_outlines_note, +}; pub use pages::{PageInfo, PdfInfo, has_illustrator_data, illustrator_data, info, is_postscript}; pub use post::{THUMBNAIL_SIZE, Thumbnail}; pub use presets::*; diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 602b09aa4..23fd60f9e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -654,7 +654,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur In a `.ai` or EPS opened from its editing data, type has its text object's opacity and blend mode, whether it is made from the file's text document or taken from the page. Where such type has transparency and comes in with other art (the rest of its look, or page type and strokes that go with it), a group of them has it. Where the page draws its type, or the strokes around it, with transparency that is neither its text object's nor its text object's combined with that of the objects around it, the page's type and strokes keep their own instead (`crates/eps/src/import/tests_native.rs`). In SVG export without Fewer `` Elements, each character of live type that justification or Mojikumi moves (the letter gaps of a Justify All line without word spaces, the gaps between the characters of a justified Japanese line, closed-up punctuation, the space between Japanese and Latin letters) starts its own positioned ``, and type scaled horizontally on a justified line is stretched to its own width (`crates/svg/tests/text_layout.rs`). A PDF saved with Illustrator's editing data opens through it as a PDF-compatible `.ai` does, with its layers and groups and with art reaching past its page unclipped; such a PDF holds the data in `AIPDFPrivateData` streams (part of #868). A shape a PDF or `.ai` paints with a gradient (a clip path round a shading) opens as one path filled with the gradient, not a clip group round a rectangle, so Shape Builder, Live Paint and Pathfinder work on its outline (#1304). - In a `.ai` opened from its editing data, a text object on a layer that shows and doesn't print stays on that layer, made from the file's text document, even where the page doesn't draw it (`crates/eps/src/import/tests_native.rs`). + In a `.ai` opened from its editing data, a text object on a layer that shows and doesn't print stays on that layer, made from the file's text document, even where the page doesn't draw it (`crates/eps/src/import/tests_native.rs`). Such a `.ai` opens without the PDF part's warnings that text was kept as outlines when no outlined glyphs come in, and a warning counts the text objects that hold the page's type next to glyphs the page has only as outlines, which don't come in (`crates/engine/src/cmd/fileio/tests_aiimport.rs`). A PDF page whose `UserUnit` (PDF 1.6) is above 1 opens at its size in points, its numbers times the `UserUnit` (one above 75,000 counts as 75,000), and `document.pdfInfo` gives its size and boxes in points too. A `.ai` whose editing data has its artboards in the units of such a page opens from that data at the page's size, with its layers, and when the layers can't be read its pages still take the artboard layout of the data, scaled the same way (part of #1482). PDF export and `.ai` saves write a page with a side longer than 14,400 pt with a `UserUnit` (PDF 1.6), its longer side divided by 14,400 and rounded up to the next thousandth, so that no side is longer than 14,400 units (ISO 32000-1, Annex C.2); in PDF 1.3 to 1.5 files and PDF/X files such a page is written in points. Placed (linked) files in a `.ai`'s editing data come in as linked images in their place, each showing the low-resolution preview the file keeps of it until its file is found or relinked in the Links panel (a file found next to the document or at its path is read as the document opens). A file with them no longer falls back to its PDF part, so its guides, sublayers and hidden objects open (#1264; `crates/eps/src/import/ai/tests.rs`, `crates/engine/src/tests_links.rs`). In a file opened that way, a gradient with stops in the middle runs its whole ramp across its shape and a gradient object no longer passes its fill to the next path (#1390); text objects with an appearance keep their own text and frame, type of hidden layers stays hidden, and type on later artboards lands on its text objects (#1391); area type whose frame is too short for a line of the fallback font shows its first line, the frame made taller with a warning in the open result (#1392).