A linked 1-bit image mask from a .ai file keeps its fill color as a
colorize paint, so the image takes the color once its file is relinked.
Until then the image shows the missing-link placeholder, and the canvas
and PDF export colorized that placeholder too: its light tile turned
nearly transparent and its red cross took the fill color.
ImageBlob::takes_colorize is false for a preview whose pixels are not
grayscale. While it is false, the canvas draws the image and PDF export
writes it as it is, printed separations separate it as an uncolorized
image, and transparency flattening (EPS, PDF 1.3 and PDF/X-1a export,
Flatten Transparency) does not count its paint as transparency. The
paint stays on the image, and a grayscale preview is still colorized.
SVG and EPS export already write only grayscale images colorized.
VectorCraft now reads `CropAreaActive`, an `/Int` in the dictionary of
the editing data that holds the `ArtboardArray`, as the index of the
active artboard in that array. The reader ignored it before, and every
file opened on its first artboard.
The opened document starts with that artboard active, so the view the
document opens at and the Artboards panel use it. The index counts
every entry of the array, so an artboard that is left out (one with no
area) does not shift it. When the entry is missing, is not an `/Int`,
or names an artboard that does not open, the first artboard is active,
as before. A file that comes in as its PDF part, with its pages placed
where its artboards are, opens on the page of the active artboard, also
when the reading stops before the artboards. The same reader serves the
editing data of EPS and PDF files.
The active artboard is kept on the opened document only; native files
do not save it.
A linked image read from a version 7 `.ai` file can keep its path in
the classic Mac OS form, with colons between folders
(`Macintosh HD:Images:photo.tif`). The link's name was the whole path,
so the Links panel showed it as one long name, and Relink to Folder,
the search of the document's folder and Find Linked Files looked for a
file with that whole string as its name.
A path with a colon and no `/` or `\` is now read as a classic Mac OS
path. Its name is the part after the last colon, and `links.info` gives
the rest as `location`. Relink to Folder, the document-folder search and
`links.findFiles` look for that name, and Find Linked Files compares the
path's folders with the folders of the files it finds. The link keeps
the path as stored. Paths with a `/` or `\` keep those as separators,
so a file name may still hold a colon (`/Users/a/x:y.png`).
Docs: docs/mcp.md (Links), docs/file-format-parity.md (version 7
linked images).
The editing copy of a PDF-compatible .ai wrote every image as its outline
filled with its average color. Embedded images are now written with their
pixels, and linked images as placed files with a preview:
- Embedded images are rasters in the form of the version 7 specification
(section 8, https://13thmonkey.org/documentation/fonts/AI7FileFormat.pdf):
%AI5_BeginRaster, the color space, the image matrix and the XI operands
of current files, then the samples in binary in a %%BeginData section.
Gray, RGB and CMYK images keep their samples (CMYK images their ink
amounts), and see-through images their alpha channel after the color
samples. The matrix is written to ten decimals.
- Linked images are placed files (section 16): the matrix, the box of the
file's pixels and its path, %%IncludeFile, the folder relative to the
document's (when the relative path ends in the path's file name), and a
preview of at most 256 pixels a side.
- Images that can't be decoded or have more than 33,554,432 pixels (the
most the reader reads) are written as their outlines filled mid gray,
images of 16 bits a channel (CMYK TIFFs included) at 8 bits, and linked
images in opacity masks and pattern tiles as embedded images, each with
a warning.
Each of these is written in the form VectorCraft's .ai reader reads, and
the tests read the written data back through that reader.
The reader now gives an image its name and opacity mask when the
operators that end its raster (XH, Xr and the N after %AI5_EndRaster) come
before its dictionary, reads an empty relative folder as the document's
own folder, and looks for the %%EndData that ends an image's samples
after the alpha channel its XI operands give it, preferring one whose next
line is XH, so samples that hold the marker's bytes are read whole.
Tests: the round trip of every kind of image and its state, a second write
of what is read back giving the same bytes, the raster and placed-file
forms, previews of large linked images, and the engine's editing copy with
every save option; a fuzz case for damaged images and placed files.
Docs: docs/file-format-parity.md.
A legacy .ai in the version 7 form names a linked image's file with
`(path) modified XG` and draws the image with `XF`, which takes the
operands `XI` takes and has no samples after it (Adobe Illustrator File
Format Specification 7.0, sections 8.2 and 8.3). The reader ignored both
operators, so the image was dropped without a warning.
- `XF` after `XG` becomes a linked image of that file, its pixels placed
as an `XI` image of the same operands is placed. The file keeps no
pixels of it, so a placeholder stands in until the file is relinked in
the Links panel, with a note in the open result. No pixels are read,
so the image is not held to the pixel limit of `XI`'s samples.
- An `XF` without a file name before it, or with operands `XI` couldn't
read (no matrix, too few numbers, no pixels, or samples of other than
1 or 8 bits), is left out with the existing "an image VectorCraft
can't read was left out" warning. An `XG` applies only to the image
after it in the same raster object; `() 1 XG` before an embedded `XI`
keeps that image embedded.
- A PostScript page draws nothing for `XF`, so there the placeholder's
area is left out of the comparison of the layers with the page, and
the file keeps its layers. A PDF part draws the image's pixels, so a
file with one is compared with it there as before, and its layers are
not used without the PDF part.
- Linked images of different files get image keys of their own even
when they show the same pixels (the placeholder, or two identical
placed-file previews), so updating or exporting one file's image
reads that file only.
- `vectorcraft_doc::links::missing_placeholder` draws the placeholder: a
light tile, crossed out, as SVG import shows for a missing file.
Tests: reader tests for the placement, the image keys and the cases left
out, an end-to-end test of a legacy file with a linked image, a test of
a file with a PDF part, and a fuzz case for damaged files with `XG` and
`XF`. The new note has a row in each catalog that translates the
placed-file note.
Locking or hiding the layer (or a group) around the isolated object left
its art selectable with the Selection tool: the isolation branch of the
hit test only checked for a locked artboard, and the marquee's checked
nothing. Both now pass over isolated art that isn't editable.
The enabled check of artboard.deleteEmpty built the full list of empty
artboards, measuring the visual bounds of the art against every
artboard, each time the Artboards panel menu was drawn. It now returns
false at once in a document with one artboard (that artboard is never
deleted). With two or more, it asks the new
Document::any_artboard_without_art whether any unlocked artboard has no
art, which is the same answer as a non-empty list.
Document::any_artboard_without_art builds the symbol boxes once for all
the artboards and first tests each path and compound path by a quick
box: its control points' box grown by the farthest its strokes reach
(Appearance::outset), with a margin for rounding. An artboard that no
quick box meets is reported empty without measuring along any path.
Only when every artboard may hold art is each one measured.
Document::artboard_has_art also skips measuring an object whose quick
box misses the artboard; its answers are unchanged.
The check of gradient.presets.save found the proxy paints through
Node::proxy_paint, which measures the object's geometric bounds for a
gradient box the check does not use. Node::proxy_paint_only picks the
same paint without measuring, and the engine's proxy_paint uses it, so
the Fill and Stroke proxies and the mixed-paint test no longer measure
each object either.
With an open stroked path of 200,000 anchors selected, the two checks
take under 0.01 ms instead of 21.9 ms and 17.2 ms in a release build, and
Delete Empty Artboards' check takes 0.85 ms instead of 42.1 ms with a
second, empty artboard.
Tests compare each check with what its command does (one artboard, all
empty, one empty, locked empty, nested art; with and without a
gradient), compare any_empty_artboards with the previous check and with
measuring every object on 120 seeded documents, compare the quick boxes
with the visual bounds on seeded strokes, and compare proxy_paint_only
with proxy_paint on seeded objects.
Docs: docs/target-app-parity.md, one line under Delete Empty Artboards.
The renderer keeps the path of a path or compound path of 1,024 anchors
or more by object id, with the bounds of its segments and of its miter
spikes in runs of 128 segments. When anchors move and the subpaths and
anchor counts stay the same, the next frame rewrites only the path
elements of the segments that start or end at a moved anchor, and
measures again only the runs that hold them and the runs of the
segments with length before them. Other edits make the path again as
before. The pixels and the cull bounds are the same.
- StrokeLayer::bounds_on is unchanged. The miter tip of one joint
(miter_tip) and the stroke box with given tips (bounds_with_tips) are
new functions beside it, and Appearance::stroked_bounds_by takes the
box of each shaped stroke, so the cull bounds of a kept path use the
kept tips.
- geom::bez::has_length is the predicate reach.rs used for segments
that draw, made public.
Docs: docs/target-app-parity.md.
An instance of a symbol whose art holds a 9-slice instance draws that
instance through its guides with the transform of every instance above
it. When the outer symbol has no guides, the quick box of an outer
instance that scales or turns its art was its art box placed by its
transform, and the renderer could cull art the nested guides move past
that box.
Symbol art boxes now record whether the art holds an instance of a
symbol with 9-slice guides at any depth. For such art, quick boxes
place the art by the instance's transform and measure it, once for each
symbol and linear part. One set of boxes places at most
MAX_SYMBOL_NODES objects this way, the art of opacity masks included.
Leaving opacity-mask or symbol editing now removes the editing layer
wherever it is, so a save or export after Layers moved it into another
layer no longer writes it. An editing id that names anything other than
a layer is left alone.
symbol_edit is no longer read from files: a value in file data is
dropped on load, so the layer it names is kept and saving does not
write the value again.
An object in 9-slice art that crosses a guide and draws symbol art is
placed by the map of the cell its center is in, so it can draw outside
the instance's transformed art box. Bounds measured without placing the
art (the renderer's culling, and placements past the object limit) used
that box alone, so the displaced art could be culled and not drawn.
The fallback now takes in the art box placed by each of the nine cells'
maps (NineSliceMap::placement_bounds), grown by the art's reach as
before.
#5139 (in 0.21.0) made a positive Twist angle turn clockwise, but
documents saved by earlier releases stored their angles with the other
sign and reopened twisted the other way.
The document version (FORMAT_VERSION) goes from 1 to 2. Opening a
version 1 document negates its Twist angles (in layers, opacity-mask
art, Shaper edits, symbols, graph designs, pattern swatches, graphic
styles and brush definitions), unless the file is of the current file
version and its generator is VectorCraft 0.21.0 or later. The document
opens and saves as version 2. A save for an older file version, which
only apps before 0.21.0 read, writes a version 1 document with the
angles negated back. Graphic style libraries (.vcstyles) go to version
2: a version 1 library's Twist angles are negated when it is read.
Readers do not check either version, so older builds open the new files
as before.
In a threaded frame whose first paragraph continues from the frame
before, the text at the frame's start is not the start of a paragraph,
and the frame draws no list marker for it. Typing "* " or "1. " there
removed the characters and set a list that never showed. typed_list_at
now returns None for that paragraph.
A graph keeps the paint and stroke weight given to a series' marks, but not
the opacity of their fill or stroke row. An opacity set with
`transparency.set {item}` or `appearance.setItem {opacity}` on a column,
bar, line or other series path was lost the next time the graph was drawn
(a data, type or option change).
- `SeriesPaint` gains `fillOpacity` and `strokeOpacity`. A setItem opacity
on the topmost fill or stroke of a series path stores it, in the same
undo step, and the generator puts it on the topmost fill and stroke of
every path directly in that series' group. Graph designs keep their own
opacity. A value read from a file is held to 0..1.
- The Appearance commands that change the opacity of a series path's
topmost fill or stroke (add, remove, duplicate and move a row, Clear
Appearance, Reduce to Basic Appearance, `appearance.transfer` and the
Eyedropper) update the stored opacity. They do not change the stored
paint, which only `paint.setFill` and `paint.setStroke` set.
- An edit that leaves a series path fully opaque stores nothing for a
series the graph keeps nothing for.
Add Anchor Points builds the new anchor list in one pass, Swatch Library add looks names up in sets, full_documents dedups placed keys with a set, width profile lookups and mark insertion use binary search, and the symbolism tools cap and thin the previews of a long drag as Liquify does.
A projection now counts as invertible when its determinant, or its
adjugate's determinant, is more than 1e-15 times the largest of the six
products that determinant sums. The adjugate of the adjugate is the map
times a number, so the inverse of a projection that inverts also inverts,
is stored and reads back. The adjugate's cofactors are formed with exact
products, so the inverse of that inverse gives back the original map up
to scale, within 1e-6 of each row's largest entry, and a map whose third
row is the sum of its first two is still refused.
Homography::conjugated and Node::transform_projection judged the affine
map by an absolute determinant of 1e-15, so a scale by 1e-8 left type and
symbol instances in perspective with their old projection. Both now use
Homography::affine_is_invertible, which compares the determinant with
the two products it sums at any scale of the map. An affine map whose
determinant or inverse is not finite, such as a scale by 1e200 or
1e-200, is inverted through Homography::inverse. When the direct product
underflows to a map that is not invertible, as for a projection stored
at a subnormal scale, conjugated forms it again with its powers of two
kept apart.
- crates/geom/src/projective.rs: the_inverse_of_an_invertible_map_is_invertible,
conjugating_does_not_depend_on_the_scale_of_the_affine,
conjugating_a_map_at_a_tiny_scale_gives_an_invertible_map
- crates/doc/src/node/tests.rs: a_projection_follows_the_transform_or_is_removed
covers an invertible scale by 1e-8 and a singular map at that size
A compound path was assembled from its members in every frame it was
drawn, and Node::stroke_path grew its buffer by doubling, copying the
path into ever larger buffers for a compound path of many anchors. The
renderer's geometry cache was swept only once it held more than 1,024
entries, so while an object was dragged (a new node, and layer, in
every frame) the old copies stayed alive with all their members.
Now:
- a compound path is drawn from the path the renderer keeps for it in
its geometry cache, as a plain path already was, and is assembled
only when the node changes;
- Node::stroke_path sizes a compound path's buffer up front from its
members' segment counts (the same elements, in the same order);
- unused geometry entries are dropped after a few frames however few
entries there are.
Tests:
- crates/render/src/tests/memory.rs: the second frame draws the kept
path and the same pixels as the first and as a new renderer; after a
five-frame move the renderer no longer holds the compound path drawn
before it. The test fails without the renderer change.
- crates/doc/src/node/tests.rs: a compound path's path is its members'
paths in order (open, closed, empty and single-anchor members).
Not timed; vectorcraft-cli perf was not rerun.
Every click and hover measured each path and compound path along its
curves to cull it (Node::reach_bounds_with), and a compound path was
then assembled whole to test its outline and fill. For a compound path
of thousands of members that took milliseconds per click.
Hit testing now culls paths and compound paths by the box of their
control points (which holds the curves), grown by their strokes' reach,
so the box is never smaller than the reach box it replaces. A compound
path's outline and fill are tested against only the members whose
control box, grown by the tolerance, holds the point: a member outside
that box is farther than the tolerance and winds zero times round the
point. Painted strokes still follow the whole compound path (alignment,
dashes, width profiles) and are measured along all of it, as before.
Tests (crates/doc/src/hit/tests.rs):
- a 2,000-member compound path hits exactly as the whole-path test did,
at random points, on and next to anchors and on the edges of the
members' boxes, for both fill rules, in Outline view and with Object
Selection by Path Only;
- wide strokes painting past the control points are still hit, for a
path and a compound path;
- the cull box holds the reach box for paths and compound paths.
Measured in a debug test build (one run, machine not idle): a click on
a 20,000-member compound path went from about 18 ms to about 1 ms.
vectorcraft-cli perf was not rerun.
With one Objects on Path group of two or more objects selected, the
Selection and Objects on Path tools show a square Space widget beyond the
last object, ahead along the path. Dragging it moves the last object
along the path, around its bends, as far as the pointer moves along it,
and the objects between follow evenly while the first one stays. With
Alt (Option on macOS) the objects spread from both ends toward or away
from the center of an open path, or of the first anchor of a closed one.
The distance between the objects shows next to the pointer in the
document's units; the drag is one undo step, and Escape cancels it.
- object.objectsOnPath.options takes spacing (points, or "auto", the
default even spread) and start (points from the first anchor). A
spacing the path has no room for is drawn smaller and kept, so a
longer path brings it back. Older files read as Auto.
- Moving the group, Pivot, Rotate, path edits, copies and saving keep
the Space. Scaling, rotating or shearing the group keeps the first
and last objects where the transform takes them on the new path, live
shapes included; a value that cannot be kept that way scales with the
path's length.
- The Rotate All widget follows the first object when Space moves it.
Behavior: https://helpx.adobe.com/illustrator/desktop/manage-objects/arrange-objects/rearrange-and-edit-objects-on-a-path.html
and https://helpx.adobe.com/illustrator/using/objects-on-path.html.
Character Style Options and New Character Style get a Character Color
section and an OpenType Features section.
Character Color sets the fill and stroke (None or a document swatch;
a global swatch or a tint of one stays linked, with a Tint from 0% to
100%), the stroke Weight, and Overprint Fill and Overprint Stroke. A
gradient or pattern paint the style already has stays until another
paint is chosen.
OpenType Features sets Figure, Position, Standard and Discretionary
Ligatures, Contextual, Stylistic and Titling Alternates, Swash,
Ordinals, Fractions and Stylistic Sets 1 to 20. A changed control
replaces only its own tags in the style's feature list, so other tags
stay, and Default Figure and Default Position are stored as their
features turned off, distinct from unset.
Empty controls stay unset, and each section's Reset Panel clears only
its own attributes.
charStyle.new, charStyle.edit and charStyle.setAttrs check a changed
fill, stroke, stroke weight or feature list: tints from 0 to 1, links
to global swatches of the document (the color follows the swatch),
weights from 0 to 4000000, and known feature tags, at most 256. Values
the style or the text it is made from already has are kept.
charStyle.duplicate keeps the copied style's values in the same way, so
a style with a language tag that is not in the list can be duplicated.
Editing a global swatch now also updates the fill and stroke stored in
character styles, so their text no longer shows as overriding the style
and text styled later takes the new color. swatch.unused counts a swatch
a character style links to as used.
Count nine-slice symbol art with clips off, as place_symbol_art_unclipped
places it, so art crossing the guides is not over-charged; add a test that
the flattening segment budget stops a path independently of the point
budget; format.
parse_range walked every index of every interval, so a range that names
the same pages many times ("1-,1-,1-,...") cost the number of parts
times the page count. Picked indices now link to the next unpicked one,
so each page is visited about once whatever the overlap.
limit() replaced each too-deep container by its children with
Vec::splice in the middle of the sibling list, moving every later
sibling each time: a wide list of such containers took quadratic time
to load. The list is now rebuilt in order from a worklist, so each node
moves a bounded number of times.
A Repeat kept up to 250 000 instances whatever its source held, and the
nested-instance count skipped the art of opacity masks. Expanding copied
the whole source for every kept instance, and a Repeat inside an enabled
mask was evaluated again for each outer copy.
The budget now counts the Repeats inside enabled opacity masks, and keeps
a second bound, MAX_REPEAT_COPIES, on the nodes and path anchors copied
in all (nested Repeats included).
A guide whose saved position leaves a center band narrower than the minimum no
longer jumps to the art's edge when a drag does not move it closer to the
opposite guide. The 9-slice scaling line in docs/target-app-parity.md no longer
says guides are not drawn in symbol editing mode.
While a symbol with Enable Guides for 9-Slice Scaling on is edited in
symbol editing mode, the canvas draws its two horizontal and two vertical
guides as dotted lines across its art, with any tool and with ruler guides
hidden. With the Selection tool the pointer near a guide shows the Move
cursor, and dragging the guide sets it through `symbol.options`'s
`nineSliceGuides` as one undo step; Escape during the drag puts it back.
A guide within 4 screen pixels takes the press before the art and the
bounding box. A dragged guide stays within the art's box and stops short
of the opposite guide; positions outside the art are not supported yet.
A character style whose Up/Down or Left/Right offset lies past MAX_COORD
either way now opens with the nearer end, as text runs already do, so
text that takes the style no longer gets the out-of-range offset. An
offset within range is not rewritten.