mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7751 from Hassan1305/fix-7741-pdf-text-stroke-cap
PDF import: stroked type keeps its cap, join and miter limit (#7741)
This commit is contained in:
@@ -27,7 +27,7 @@ use crate::import_scan::{FontNames, MAX_NESTING, Ocgs, Scan, TextOverprint, Walk
|
||||
use crate::import_shading::{
|
||||
FunctionBudget, FunctionPaint, clipped, extend_clip, fold_stop_opacity, function_paint, gradient_shape, mesh_shading, shading_gradient,
|
||||
};
|
||||
use crate::import_text::{Families, LineFacts, Look, Placement, TextLine, Upright};
|
||||
use crate::import_text::{Families, LineFacts, Look, Placement, TextLine, TypeStroke, Upright};
|
||||
use crate::{CropTo, ImportOptions, ImportReport, PdfError, TextAs};
|
||||
|
||||
mod actual_text;
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::entry::{
|
||||
UNNAMED_CHARACTERS_NOTE, max_objects,
|
||||
};
|
||||
use super::reshaped;
|
||||
use super::state::{Builder, Frame, FrameKind, GlyphRun, Saved, bounds, grouped, line_width, mean_scale, paints, round3, same_stroke, stroke_layer};
|
||||
use super::state::{Builder, Frame, FrameKind, GlyphRun, Saved, bounds, grouped, mean_scale, paints, round3, same_stroke, stroke_layer, type_stroke};
|
||||
use super::{
|
||||
Affine, Appearance, AppearanceItem, Arc, BezPath, BfString, BlendMode, CacheKey, Color, Colors, Families, FillLayer, FillRule, FunctionBudget,
|
||||
FunctionPaint, HashMap, HashSet, ImageBlob, ImageObject, Knockout, Look, MaskSpec, MaskType, Native, Node, NodeId, NodeKind, OpacityMask, Paint,
|
||||
@@ -961,7 +961,7 @@ impl<'p> Builder<'p> {
|
||||
}
|
||||
None => (None, 1.0),
|
||||
};
|
||||
let stroke = stroke.zip(paint.clone()).map(|(p, paint)| (paint, line_width(p, scale)));
|
||||
let stroke = stroke.zip(paint.clone()).map(|(p, paint)| type_stroke(paint, p, scale));
|
||||
let look = Look {
|
||||
font: font_key,
|
||||
family: info.family,
|
||||
@@ -1109,7 +1109,7 @@ impl<'p> Builder<'p> {
|
||||
};
|
||||
// What the glyph is drawn with as an outline, should its line be kept so.
|
||||
let drawn = paint.clone().map(|p| (p, stroke.cloned()));
|
||||
let stroke = stroke.zip(paint.clone()).map(|(p, paint)| (paint, line_width(p, scale)));
|
||||
let stroke = stroke.zip(paint.clone()).map(|(p, paint)| type_stroke(paint, p, scale));
|
||||
let look = Look {
|
||||
font: key,
|
||||
family: info.family,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use super::cid::{CidText, FontInfo};
|
||||
use super::{
|
||||
Affine, Arc, BezPath, BlendMode, Colors, Dash, Families, FillRule, FontNames, FunctionBudget, HashMap, HashSet, ImageBlob, LineCap, LineFacts,
|
||||
LineJoin, LumaData, MaskSpec, Node, NodeId, Paint, PathEl, PatternDef, Rect, Scan, StrokeLayer, StrokeProps, TextAs, TextLine,
|
||||
LineJoin, LumaData, MaskSpec, Node, NodeId, Paint, PathEl, PatternDef, Rect, Scan, StrokeLayer, StrokeProps, TextAs, TextLine, TypeStroke,
|
||||
};
|
||||
|
||||
/// Decoded RGBA pixels, width, height and hayro's scale factors.
|
||||
@@ -277,6 +277,13 @@ pub(super) fn stroke_layer(paint: Paint, opacity: f32, p: &StrokeProps, scale: f
|
||||
st
|
||||
}
|
||||
|
||||
/// The stroke `p` draws type with in `paint` at `scale`: width, cap, join and miter limit as
|
||||
/// [`stroke_layer`] takes them (#7741).
|
||||
pub(super) fn type_stroke(paint: Paint, p: &StrokeProps, scale: f64) -> TypeStroke {
|
||||
let st = stroke_layer(paint, 1.0, p, scale);
|
||||
TypeStroke { paint: st.paint, width: st.width, cap: st.cap, join: st.join, miter_limit: st.miter_limit }
|
||||
}
|
||||
|
||||
/// The most entries of a dash array kept (an even count keeps it a pattern of dashes and gaps).
|
||||
const MAX_DASH_ENTRIES: usize = 64;
|
||||
/// Most dash periods [`stroke_outline`] cuts a stroke into.
|
||||
|
||||
@@ -13,7 +13,7 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use kurbo::{Affine, BezPath, Point, Rect, Vec2};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{CharStyle, Node, ParaDirection, TextKind, TextObject, TextRun};
|
||||
use vectorcraft_doc::{CharStyle, LineCap, LineJoin, Node, ParaDirection, TextKind, TextObject, TextRun};
|
||||
use vectorcraft_text::TextLayout;
|
||||
|
||||
/// One glyph's placement: its baseline origin, advance direction, size and horizontal scale.
|
||||
@@ -70,8 +70,19 @@ pub(crate) struct Upright {
|
||||
pub top: Point,
|
||||
}
|
||||
|
||||
/// A stroke on type: its paint and width, and the cap, join and miter limit the file strokes the
|
||||
/// glyphs with (#7741).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct TypeStroke {
|
||||
pub paint: Paint,
|
||||
pub width: f64,
|
||||
pub cap: LineCap,
|
||||
pub join: LineJoin,
|
||||
pub miter_limit: f64,
|
||||
}
|
||||
|
||||
/// What a run of type is drawn with: its font (cache key, family and style), size, horizontal
|
||||
/// scale, fill and stroke (paint and width).
|
||||
/// scale, fill and stroke.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Look {
|
||||
pub font: u128,
|
||||
@@ -82,7 +93,7 @@ pub(crate) struct Look {
|
||||
pub size: f64,
|
||||
pub h_scale: f64,
|
||||
pub fill: Option<Paint>,
|
||||
pub stroke: Option<(Paint, f64)>,
|
||||
pub stroke: Option<TypeStroke>,
|
||||
/// The fill, the stroke overprints.
|
||||
pub overprint_fill: bool,
|
||||
pub overprint_stroke: bool,
|
||||
@@ -102,7 +113,11 @@ impl Look {
|
||||
}
|
||||
|
||||
fn style(&self) -> CharStyle {
|
||||
let (stroke, stroke_width) = self.stroke.clone().unwrap_or((Paint::None, 0.0));
|
||||
let default = CharStyle::default();
|
||||
let (stroke, stroke_width, stroke_cap, stroke_join, stroke_miter_limit) = match self.stroke.clone() {
|
||||
Some(s) => (s.paint, s.width, s.cap, s.join, s.miter_limit),
|
||||
None => (Paint::None, 0.0, default.stroke_cap, default.stroke_join, default.stroke_miter_limit),
|
||||
};
|
||||
CharStyle {
|
||||
font_family: self.family.clone(),
|
||||
font_style: self.style.clone(),
|
||||
@@ -112,9 +127,12 @@ impl Look {
|
||||
fill: self.fill.clone().unwrap_or(Paint::None),
|
||||
stroke,
|
||||
stroke_width,
|
||||
stroke_cap,
|
||||
stroke_join,
|
||||
stroke_miter_limit,
|
||||
overprint_fill: self.overprint_fill,
|
||||
overprint_stroke: self.overprint_stroke,
|
||||
..CharStyle::default()
|
||||
..default
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,6 +205,9 @@ pub(crate) struct TextLine {
|
||||
/// Runs of glyphs drawn alike: their look, how far the file raised them off the line's
|
||||
/// baseline (points, up; text rise, `Ts`), their text.
|
||||
runs: Vec<(Look, f64, String)>,
|
||||
/// Where the last glyph's text starts in the last run (bytes): a stroke over that glyph
|
||||
/// alone restyles only its text ([`stroke_last`](Self::stroke_last)).
|
||||
last_start: usize,
|
||||
/// The bytes of text the horizontal glyphs added to `runs` so far (counted as they come: a
|
||||
/// sum over the runs for each glyph is quadratic when every glyph is a run).
|
||||
text_bytes: usize,
|
||||
@@ -257,6 +278,7 @@ impl TextLine {
|
||||
last_size: at.size,
|
||||
baseline: vec![],
|
||||
runs: vec![],
|
||||
last_start: 0,
|
||||
text_bytes: 0,
|
||||
slots: vec![],
|
||||
authored: vec![],
|
||||
@@ -303,6 +325,7 @@ impl TextLine {
|
||||
let size = self.at.size.max(at.size);
|
||||
let Some((last, _, run)) = self.runs.last_mut() else {
|
||||
self.runs.push((look.clone(), 0.0, text.to_string()));
|
||||
self.last_start = 0;
|
||||
self.record_actual_fill(look, at, content);
|
||||
self.authored.extend(text.chars().map(|_| content.authored()));
|
||||
(self.upright, self.next, self.last) = (Some(upright), at.origin + down * at.size, at.origin);
|
||||
@@ -334,9 +357,11 @@ impl TextLine {
|
||||
&& (matches!(content, TextContent::Pen)
|
||||
|| (!matches!(content, TextContent::Actual(_)) && !self.authored.last().copied().unwrap_or(false)))
|
||||
{
|
||||
self.last_start = run.len();
|
||||
run.push_str(text);
|
||||
} else {
|
||||
self.runs.push((look.clone(), 0.0, text.to_string()));
|
||||
self.last_start = 0;
|
||||
}
|
||||
self.record_actual_fill(look, at, content);
|
||||
self.authored.extend(text.chars().map(|_| content.authored()));
|
||||
@@ -425,12 +450,15 @@ impl TextLine {
|
||||
&& (*last_shift - shift).abs() < 1e-3
|
||||
&& (pen || (!matches!(content, TextContent::Actual(_)) && !self.authored.last().copied().unwrap_or(false)))
|
||||
{
|
||||
self.last_start = run.len();
|
||||
run.push_str(text);
|
||||
} else {
|
||||
self.runs.push((look.clone(), shift, text.to_string()));
|
||||
self.last_start = 0;
|
||||
}
|
||||
} else {
|
||||
self.runs.push((look.clone(), 0.0, text.to_string()));
|
||||
self.last_start = 0;
|
||||
}
|
||||
self.record_actual_fill(look, at, content);
|
||||
self.text_bytes = self.text_bytes.saturating_add(text.len());
|
||||
@@ -463,7 +491,7 @@ impl TextLine {
|
||||
/// The reader fills a whole Tj/TJ before replaying its glyphs as strokes. A replay
|
||||
/// must neither consume the replacement nor rewind the pen. Only the first glyph's
|
||||
/// style belongs to the replacement; suppressed glyphs have no text to restyle.
|
||||
pub fn stroke_actual(&mut self, font: u128, at: Placement, stroke: (Paint, f64), overprint: bool) -> bool {
|
||||
pub fn stroke_actual(&mut self, font: u128, at: Placement, stroke: TypeStroke, overprint: bool) -> bool {
|
||||
let key = (font, at.origin.x.to_bits(), at.origin.y.to_bits());
|
||||
let Some(run) = self.actual_fills.remove(&key) else { return false };
|
||||
if let Some((look, ..)) = run.and_then(|i| self.runs.get_mut(i)) {
|
||||
@@ -501,13 +529,34 @@ impl TextLine {
|
||||
Some(bp)
|
||||
}
|
||||
|
||||
/// A stroke (`stroke`: paint and width) over the glyph just drawn at `at` in `font` (fill
|
||||
/// and stroke rendering), which `overprint`s or not: the run is stroked. `false`: it isn't
|
||||
/// that glyph.
|
||||
pub fn stroke_last(&mut self, font: u128, at: Placement, stroke: (Paint, f64), overprint: bool) -> bool {
|
||||
let Some((look, ..)) = self.runs.last_mut().filter(|(l, ..)| l.font == font && (at.origin - self.last).hypot() < self.at.size * 1e-3) else {
|
||||
/// A stroke (`stroke`: paint, width, cap, join and miter limit) over the glyph just drawn at
|
||||
/// `at` in `font` (fill and stroke rendering), which `overprint`s or not: that glyph is
|
||||
/// stroked. `false`: it isn't that glyph.
|
||||
///
|
||||
/// A filled glyph joins a run of the same fill whatever its stroke ([`Look::takes`]), so the
|
||||
/// run can hold glyphs before it that are stroked otherwise or not at all: then the glyph's
|
||||
/// text becomes a run of its own with this stroke, and the glyphs before it keep theirs
|
||||
/// (#7741). The line's text is the same either way, so the characters' slots, gaps and
|
||||
/// authored flags, kept along the whole line, still line up.
|
||||
pub fn stroke_last(&mut self, font: u128, at: Placement, stroke: TypeStroke, overprint: bool) -> bool {
|
||||
let start = self.last_start;
|
||||
let Some((look, shift, text)) =
|
||||
self.runs.last_mut().filter(|(l, ..)| l.font == font && (at.origin - self.last).hypot() < self.at.size * 1e-3)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let same = look.stroke.as_ref() == Some(&stroke) && look.overprint_stroke == overprint;
|
||||
// Only the last glyph's text in the run (`start` within it, on a character boundary):
|
||||
// split it off. A run of that glyph alone is stroked whole, as before.
|
||||
if !same && start > 0 && start < text.len() && text.is_char_boundary(start) {
|
||||
let tail = text.split_off(start);
|
||||
let mut own = look.clone();
|
||||
(own.stroke, own.overprint_stroke) = (Some(stroke), overprint);
|
||||
let shift = *shift;
|
||||
self.runs.push((own, shift, tail));
|
||||
self.last_start = 0;
|
||||
return true;
|
||||
}
|
||||
(look.stroke, look.overprint_stroke) = (Some(stroke), overprint);
|
||||
true
|
||||
}
|
||||
|
||||
@@ -55,3 +55,70 @@ fn an_aligned_character_stroke_is_clipped_to_its_side_of_the_glyphs() {
|
||||
assert!(ops.contains(&if clip { "W*" } else { "W" }), "{align:?}: clipped to its side");
|
||||
}
|
||||
}
|
||||
|
||||
/// The cap, join and miter limit a PDF strokes its type with, whatever the rendering mode that
|
||||
/// strokes it (stroke, fill and stroke, and either with a clip), and with replacement text
|
||||
/// (`ActualText`) too: not butt caps and miter joins (#7741).
|
||||
#[test]
|
||||
fn imported_stroked_type_keeps_the_files_cap_join_and_miter_limit() {
|
||||
use vectorcraft_testkit::pdf::{PdfPage, pdf};
|
||||
let page = |content: &str| {
|
||||
let resources = "/Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >>";
|
||||
pdf(&[PdfPage { resources: resources.into(), ..PdfPage::new(300.0, 200.0, content) }], None)
|
||||
};
|
||||
let styles = |content: &str| {
|
||||
let d = import(&page(content)).unwrap();
|
||||
let mut runs = vec![];
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
runs.extend(t.runs.iter().map(|r| r.style.clone()));
|
||||
}
|
||||
});
|
||||
runs
|
||||
};
|
||||
let cases = [
|
||||
("1 J 1 j", (LineCap::Round, LineJoin::Round, 10.0)),
|
||||
("2 J 2 j 3 M", (LineCap::Square, LineJoin::Bevel, 3.0)),
|
||||
("0 J 0 j 3 M", (LineCap::Butt, LineJoin::Miter, 3.0)),
|
||||
];
|
||||
for (settings, want) in cases {
|
||||
for mode in ["1", "2", "5", "6"] {
|
||||
// A word shown at once, a glyph at a time (a stroke over the glyph just filled), and
|
||||
// in a span with replacement text.
|
||||
for text in ["(WAVE) Tj", "(W) Tj (A) Tj", "/Span << /ActualText (WAVE) >> BDC (WAVE) Tj EMC"] {
|
||||
let content = format!("0 g 0 0 0 RG 4 w {settings} BT {mode} Tr /F1 60 Tf 40 110 Td {text} 0 Tr ET");
|
||||
let runs = styles(&content);
|
||||
let stroked: Vec<_> = runs.iter().filter(|s| !s.stroke.is_none()).collect();
|
||||
assert!(!stroked.is_empty(), "{content}: stroked type comes in: {runs:?}");
|
||||
if matches!(mode, "2" | "6") && !text.starts_with("(WAVE)") {
|
||||
assert!(stroked.iter().any(|s| !s.fill.is_none()), "{content}: the stroke is over the filled glyphs: {runs:?}");
|
||||
}
|
||||
for s in stroked {
|
||||
assert!((s.stroke_width - 4.0).abs() < 1e-6, "{content}: width {}", s.stroke_width);
|
||||
let got = (s.stroke_cap, s.stroke_join, s.stroke_miter_limit);
|
||||
assert_eq!(got, want, "{content}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Glyphs stroked with another join are a run of their own.
|
||||
let runs = styles("0 0 0 RG 4 w BT 1 Tr /F1 60 Tf 40 110 Td 1 j (WA) Tj 0 j (VE) Tj 0 Tr ET");
|
||||
let joins: Vec<_> = runs.iter().map(|s| s.stroke_join).collect();
|
||||
assert_eq!(joins, [LineJoin::Round, LineJoin::Miter]);
|
||||
// Filled and stroked a glyph at a time: the next glyph's fill joins the run before it, and its
|
||||
// stroke, with another join, restyles that glyph alone.
|
||||
for mode in ["2", "6"] {
|
||||
let content = format!("0 g 0 0 0 RG 4 w BT {mode} Tr /F1 60 Tf 40 110 Td 1 j (W) Tj 0 j (A) Tj 0 Tr ET");
|
||||
let runs = styles(&content);
|
||||
let d = import(&page(&content)).unwrap();
|
||||
let mut text = String::new();
|
||||
d.walk(|n| {
|
||||
if let NodeKind::Text(t) = &n.kind {
|
||||
text.push_str(&t.plain_text());
|
||||
}
|
||||
});
|
||||
assert_eq!(text, "WA", "{content}");
|
||||
let joins: Vec<_> = runs.iter().map(|s| (!s.stroke.is_none()).then_some(s.stroke_join)).collect();
|
||||
assert_eq!(joins, [Some(LineJoin::Round), Some(LineJoin::Miter)], "{content}: {runs:?}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (M10/G5: Neon & Glow built-in graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); previously: Tech Circuit brush library, G5; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-15 · **Change:** minor (PDF import: stroked type keeps the line cap, line join and miter limit the file strokes it with, a glyph stroked otherwise than the run it joins coming in as a run of its own, #7741; M10/G5: Neon & Glow built-in graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); previously: Tech Circuit brush library, G5; previously: Graphic Styles list fills a taller panel, #2992; scores and estimates unchanged; previously: PDF import: glyphs in a span with an empty ActualText come in as outlines, #6971; scores and estimates unchanged; previously: library panels' lists fill a taller panel, #2992; scores and estimates unchanged; previously: Swatches list fills a taller panel, #1638; scores and estimates unchanged; previously: icon-strip flyouts also resize from the top edge and the canvas-side top corner, keeping the moved top in workspaces (storytold/craft-libs#1094); scores and estimates unchanged; previously: #6865: explicitly requested oversized editing artboard; Live Paint Bucket fills faces crossed in one drag (#6373); previously: #6404: transform-aware Eyedropper character copying; scores and estimates unchanged; previously: M10/G5: Energy Beams brush library; MCP docs list Energy Beams; previously minor: M10/G5: Skill Icons symbol library; MCP docs list Skill Icons; previously minor: M10/G5: Status Effects graphic style library; MCP docs catch-up for symbol libraries; previously minor: M10/G5: Inventory Icons built-in symbol library; previously minor: Edit › Edit Custom Dictionary…: its dialog adds, deletes and changes the custom dictionary's words, #4936; previously minor: M10/G5: Game HUD graphic style library; previously minor: resizable icon-strip flyouts and bottom-aligned panel footers, #5713; previously minor: M10/G5: Map Markers built-in symbol library; previously minor: right-click on type: Spelling › Auto Spell Check, with its check mark, #4936; previously minor: right-click on a word Auto Spell Check underlined: its suggestions, Ignore All and Add to Dictionary, #4936; previously minor: G2: Effect Gallery thumbnails and in-dialog preview are in; Auto Spell Check: `edit.autoSpellCheck` underlines the unknown words, #4936; previously minor: PDF editable import preserves authored ActualText once, including empty/nested spans and authored ordering (#6165); cross-line/style layout remains approximate; scores and estimates unchanged; PDF marked-text export (#6309): compatible glyphs stay on one layout line with per-glyph offsets; scores and estimates unchanged; interim macOS cursor delivery, #2292; native validation pending; scores and estimates unchanged; Control bar › Snapping includes the existing Snap to Glyph and Alignment Guides toggles (#6164); scores and estimates unchanged; custom dictionary and Ignore All: `text.spelling.addWord`, `removeWord`, `changeWord`, `dictionary` and `ignoreAll`, #4936; previously minor: EPS import: embedded fonts (composite, Type 42, CID-keyed, Type 1, CFF FontSet) draw their glyphs; spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof, #4936; previously minor: floating tab groups collapse into the side icon rail and opened rail panels fold back into it, #5925 in part; M4/M7: `.ai`/EPS type keeps its tab stops, and right-aligned lines with tabs lay out as the file's page, #6147; M4/M7: `.ai`/EPS type from the text document lays its lines out closer to the file's page, within limits, #6137; numeric fields step on wheel hover without taking keyboard focus, #5949; Character Styles panel: a double-click on a style's row opens its Character Style Options; the dialog keeps a fixed width and scrolls its sections, so its buttons stay on screen; Selection QoL: marquee modes and visible feedback, #5313; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; #3380 deletion-only review follow-up: independent survivors keep gradient placement, owned subpaths avoid discarded copies, and contracts/status/evidence are clarified; endpoint highlighting remains open; scores and estimates unchanged; Indian-language hyphenation review and license allowlist, #4947; Hindi catalog and contributor loanword preferences; full wording review in progress; Effect dialogs: a colour swatch beside a hex colour opens the Color Picker, #5365; shape tools move the selection from its centre point and turn it from the zone outside a box corner, #5477; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x)
|
||||
>
|
||||
> Oversized artboard import update (2026-11-12): Explicit editing-data import retains a single artboard beyond a capped 14,400 pt PDF page (#6865).
|
||||
>
|
||||
@@ -159,7 +159,7 @@ Gujarati interface follow-up (2026-11-14): the catalog and zero/one plural rule
|
||||
| M1 | Selection, transform, layers, MCP (common command tools, strict keys, session resources and version-aware result hints) | ✅ mostly done (document-tab artwork drag with lossless resource transfer and one target Undo; malformed MCP app replies discard the connection without replay, #1761; bounded control socket waits and connection-slot recovery, #1726; bounded synthetic control input, #1709; no-op anchor Align keeps Undo, Redo and saved state (#1180 follow-up); transform reference point snaps to anchors/centres; rotated objects keep a rotated bounding box and their angle; Free Transform, Scale and Reflect along a rotated box pending; the Transform fields take one Tab each, the W/H link after them) | 4–6 |
|
||||
| M2 | Drawing tools + smart guides | ✅ mostly done (#3380 deletion creates independent selected path pieces and keeps bounds-fitted gradients in place; endpoint highlighting remains open; Polygon/Star exact-size count persistence, with repeated-drag, tool-switch and Cancel regression coverage; Curvature point markers without direction handles (#1236, in part); Direct Selection segment click/Delete preserves neighboring edges, #1278; Flare, Reshape, Live Corners widget dragging and per-corner radii and kinds landed, on any path's corners; Shaper rough-shape recognition and orientation steps, the remaining Pen modifier nuances; Shaper Groups with editable sources and separate fill/stroke edits landed; custom ruler origin with drag preview, active-artboard reset, relative X/Y fields and native round-trip (#1212); construction widget/refinements pending); a shape just drawn moves by its centre point and turns from the zone outside a corner with its tool still active (#5477); Tangent and straight-segment perpendicular snapping use saved Smart Guides Geometric Guides preferences and Control bar quick access (#2537), including the existing Snap to Glyph and Alignment Guides toggles (#6164); no View submenu; Copy and Cut with the Pen or Curvature tool take the anchors a Cmd/Ctrl-lent Direct Selection picked (#6123) | 12–18 |
|
||||
| M3 | Paint & appearance (swatches, color, gradient, stroke, appearance, transparency, styles) | ✅ done (M3.7–M3.98): swatches, groups, libraries, tints, Lab spots and Registration; Eyedropper copies transformed character dimensions (#6404) and styles highlighted text only (atomic transparency rejection); Eyedropper reads visible locked and outside-isolation sources (#1251); Color Picker, Color panel, Color Guide, Color Themes, Edit Colors and Recolor Artwork (screen-bounded Edit chip list, #3416); gradients that follow every transform, the annotator, linked stops, freeform gradients, focal points, gradients on strokes and Expand; visible strokes when paint is added to inherited None/zero-width art (#1253); stroke geometry, arrowheads, dashes fitted to corners, stroke on type, width profiles and the Width tool (Shift-double-click enables adjoining adjustment in Width Point Edit; finite-arithmetic follow-up regressions and full CI passed ([Validation evidence](target-app-parity.md#evidence-what-ships-today-by-area))), Scale Strokes and preview bounds; Appearance targeting, container appearance, target circles, Expand Appearance; knockout, isolation, blend accuracy, CMYK blending, overprint, Attributes, masks and clipping, Flatten Transparency; graphic styles, links and libraries; CSS library export preserves fractional linear and radial gradient stop positions. Left: freeform gradients in SVG/PDF, mixed spot/process gradients in SVG and PDF gradients mixing a spot color with RGB or Lab stops export as stops or process colors; PDF export writes dithered gradient fills with spot stops, and Perceptual spot gradients with RGB or Lab stops other than white or more than 4,096 stops on their curve, as images in process colors; confirm the Scale Strokes & Effects default; Recolor model selection is available through the engine and MCP only (#2520); the dialog retains its existing controls; Library panels' lists (swatch, Color Book, brush, symbol and graphic style libraries) fill a taller panel above its footer (#2992; scores and estimates unchanged); Graphic Styles list fills a taller panel above its footer (#2992; scores and estimates unchanged); Swatches list fills a taller panel above its footer (#1638; `the_list_fills_a_tall_panel_above_its_footer`; scores and estimates unchanged); | 4–8 |
|
||||
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | Rename artboards in Export for Screens (#7275); Explicit editing-data import keeps oversized capped-page artboards (#6865; estimates unchanged); Save refusal continuation (#6146): ordinary Save offers Save As; unchanged format keeps options, changed format opens its Save options; cancellation/automation isolation; focused UI regressions passed, native acceptance pending; PDF authored ActualText preserved once in editable import (#6165; first-glyph line/style, bounded input; estimates unchanged); PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. `.ai`/EPS type from the text document keeps its paragraphs' tab stops (#6147); `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, and may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them (#6137); ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); | 3–6 (existing scope; Affinity export unestimated) |
|
||||
| M4 | Files (native, SVG, PDF, raster, Export for Screens, clipboard interop) | PDF import: stroked type keeps the file's line cap, line join and miter limit (#7741; estimates unchanged); Rename artboards in Export for Screens (#7275); Explicit editing-data import keeps oversized capped-page artboards (#6865; estimates unchanged); Save refusal continuation (#6146): ordinary Save offers Save As; unchanged format keeps options, changed format opens its Save options; cancellation/automation isolation; focused UI regressions passed, native acceptance pending; PDF authored ActualText preserved once in editable import (#6165; first-glyph line/style, bounded input; estimates unchanged); PDF export keeps harakat and zero-advance Noto Arabic dot glyphs in one compatible batch per layout line, with per-glyph vertical offsets (#6309). `crates/pdf/src/tests/realtext.rs` (`mark_text_*`) checks decoded cluster mappings, mixed Arabic/Latin/digits, contour placement against outline export, vertical columns, differing path rotations and PDF/X-1a/3/4. Editable-text import preserves authored ActualText separately (#6165), with cross-line/style layout limits; separately styled or differently rotated clusters and external viewer search/copy remain outside this export-only evidence. `.ai`/EPS type from the text document keeps its paragraphs' tab stops (#6147); `.ai`/EPS type from the text document keeps auto leading per line, its hyphenation options, and may show more of the last lines the file sets in an area frame of its own, when growing the frame by its lines' deepest descent plus 0.05 pt shows them (#6137); ✅ done (M4.14–M4.98 on 2026-10-05): Save As/Save a Copy/Revert/templates, Data Recovery and background save, SVG import and export fidelity (units, text, symbols, filters, SVGZ, Preserve Editing, SVG Options), shared guarded PDF writer and windowed reader ([pdf-backend.md](pdf-backend.md)), PDF import (colours, layers, masks, editable text, security) and export (presets, marks, bleed, ICC and output intent, raster effects, subset fonts, PDF/X-1a, X-3 and X-4, PDF layers, overprint, page thumbnails, fast web view, PDF 1.3), Place and Links, Package, EPS and DXF in and out (EPS legacy mesh corners, vector opacity masks and raster page comparison verified; closed SPLINE flags round-trip and circular paths export as native CIRCLE/ARC, #1299), EMF/WMF, TIFF/BMP/Targa/PSD/GIF/PNG-8 export, Print and print presets, slices, Save for Web, TIFF in Export for Screens and Asset Export, clipboard flavours, File Info; AF.1–AF.4 add Affinity import (native layers, artboards, vectors, text and images from Affinity 1–3 files; the embedded preview only as a warned fallback; AF.4 expands current `.af` fixtures and every-board round-trip validation); EPS Save and Save As keep an opened EPS as EPS with its page frame and complete editable VectorCraft document (2026-10-26; G9 import fidelity unchanged). Left: DWG (use DXF), PSD placement as layers, overprint read back from PDF, Affinity export, polish; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; SVG CSS classes survive export and repeated import in all four styling modes (#2480); current-main integration retains event attributes and scripts; PDF import: glyphs inside a marked-content span with an empty `ActualText` come in as outlines in their fill and stroke, adding no text (#6971; scores and estimates unchanged); | 3–6 (existing scope; Affinity export unestimated) |
|
||||
| M5 | Performance | 🟡 background render + caches + MT done; `vectorcraft-cli bench` and `vectorcraft-cli perf` (budget suite); opaque solid-fill knockout paths draw once in hard-edge/supersampled raster export, with analytical-AA compositing unchanged; file format v2 opens 3× faster (50k paths: 722 → 244 ms); raster effects (glows, shadows, blur, feather) no longer force the whole frame single-threaded (filtered offscreen per effect, verified equal to the single-threaded reference); Craquelure/Mosaic Tiles/Stained Glass distance searches optimized with pixel-invariance checks and the `texture_drag_perf` probe (#1257); effect-heavy demos need a clean-machine benchmark; dirty-region rendering, GPU backend spike pending | 15–25 |
|
||||
| M6 | Path operations (Pathfinder, Shape Builder, offset…) | ✅ mostly done (live compound shapes: Make, Alt-click a Shape Mode, member modes, Release, Expand (#4522); single-result Pathfinder names and SVG IDs retained (#1293); Divide open-cutting-path geometry, appearance and history verified (#1307; existing segment cap remains); Simplify preview/counts and rewritten-path component selection verified (#1272; the slider opens at the Auto-Simplify level; a Direct Selection part of a path simplifies on its own); Compound appearance and member fill-rule edits verified (#1197); Boolean precision on almost-horizontal edges fixed and the property tests made deterministic; Shape Builder gap detection, large-offset bug open); Shape Builder Gap Detection shares Live Paint’s spatial gap finder and temporary merge bridges (#2526); Custom is capped at 72 pt in the existing options dialog | 3–6 |
|
||||
| M7 | Type (point/area/path, editing, styles, OpenType, threading, glyphs) | Spell check engine: `text.spelling.check` and `text.spelling.suggest` on craft-proof and the custom dictionary and Ignore All commands and Auto Spell Check with its right-click suggestions, Ignore All and Add to Dictionary, and Spelling › Auto Spell Check in the context menu of type, and the Edit Custom Dictionary dialog (#4936; the Check Spelling dialog remains); Right-aligned area type sets the text after a line's last tab against the right edge, the text before it at its stops (#6147); A line a soft return ends is justified as a wrapped line is (#6137); Indian-language pattern hyphenation preserves syllables in both composers (#4947); 🟡 Hanging punctuation keeps its published amounts under Glyph Scaling, and Standard burasagari counts a hung line start as room; Fullwidth `!` / `?` keep an upright fallback in vertical type; Hyphenation… and Justification… per paragraph (#2310, #2755), Auto Leading also driving the Character panel's Leading, the Alt+↑/↓ leading nudge and Fill with Placeholder Text; Document-point character controls after object/group scaling (#1034; optional font-size preservation remains separate), Character/Paragraph Styles (explicit Auto-leading resets and local leading overrides), per-paragraph paragraph attributes, mixed paragraph alignment indication (headless regressions, full integration CI and visual review pass), Area Type Options (with Width/Height, Auto Size and Shrink Text to Fit; overflow reported to agents and vertical Align), type areas resized by their handles and reshaped with Direct Selection, threaded text, Fit Headline, Glyphs (feature-valued alternates and unencoded glyphs applied in one Undo step while keeping Arabic joining, #5514), OpenType panel (including font-defined Stylistic Sets), Find Font (Document, Recent and System replacement sources, #5217), installed system fonts in every font menu (fonts registered with Windows outside its font folders, the XDG and Flatpak host font folders on Linux, and fonts installed while the app runs, listed when it comes back to the front; searchable; the Character and Properties menus show each family's sample of the selected text, preview it live under the pointer or ↑/↓, show recently applied families first (#3515), filter by script and kind, list a family's styles, and star favourites; the Glyphs panel and Find Font's Replace With use the same menu without the live preview) and found by name on open/import (full-name aliases and Missing Fonts searches verified, #1258; Windows font-service QA pending), variable fonts' named instances as styles, CJK names in the UI, input methods (marked text in place, candidate window at the caret; Japanese IME), Text Wrap (offset, invert, both sides of an object; follows edits), Type on a Path effects and Options with draggable start/centre/end brackets (slide, flip), tab stops + Tabs panel (Above Text floats it over the selected text, its ruler at the canvas zoom so stops line up with the columns, #729), inline symbols in text (beyond parity), Hebrew/Arabic bidirectional text, Paragraph Direction, Every-line/Single-line Composer per object (ragged text too) done; paint-only Arabic harakat keep joining with GDEF-mark ownership and Insert Kashidas tatweels on the base letter's paint (`tests::harakat`); tab leaders, spell check, vertical type pending | 45–60 |
|
||||
@@ -207,6 +207,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-15 | minor | PDF import: stroked type (text rendering modes 1, 2, 5 and 6, and glyphs under `ActualText`) keeps the line cap, line join and miter limit of the graphics state it is stroked with, as paths do, instead of butt caps, miter joins and a miter limit of 10; a glyph filled and then stroked otherwise than the run its fill joined comes in as a run of its own, so the glyphs before it keep their stroke (#7741; `imported_stroked_type_keeps_the_files_cap_join_and_miter_limit` in `crates/pdf/src/tests/textstroke.rs`). Scores and estimates unchanged. |
|
||||
| 2026-11-15 | minor | M10/G5: Neon & Glow built-in graphic style library (Cyan Neon, Magenta Laser, Amber Tube, Electric Lime, Pulse Violet, Cobalt Phosphor); estimates unchanged |
|
||||
| 2026-11-14 | minor | Tech Circuit adds three original procedural brushes (Circuit Trace, Bus Lines, Terminal Nodes); ten built-in brush libraries. G5 remaining estimate 3–13 h (estimated); parity scores and headline totals unchanged. |
|
||||
| 2026-11-14 | minor | Gujarati (#6613): contributor proofreading completed for all 387 additions, with 11 wording corrections; 4,673 reviewed catalog rows. Status/error messages remain partial; web selection requires a bundled Gujarati face. Scores and estimates unchanged. |
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user