Commit Graph
74 Commits
Author SHA1 Message Date
dotCooCoo de8ac51502 Symbols: a stained instance keeps its stain in SVG, PDF and the other exports
A symbol instance colored with the Symbol Stainer lost its stain in
export whenever the canvas's stained art was not used:

- An instance with geometry effects was reshaped for export from the
  symbol's art with only its dynamic changes, so SVG, PDF, EPS, .ai,
  DXF, EMF/WMF and HPGL wrote the unstained colors. A color adjustment
  on an instance recolored the unstained art the same way.
- PDF, EPS, legacy .ai, DXF and EMF/WMF wrote an instance without
  effects from the unstained art.

tint_node, stain and instance_art now live in
vectorcraft_doc::symbol_overrides, which both the effects and the
exporters can use; vectorcraft_brush re-exports them. Baking, color
adjustments and the writers above now use instance_art: dynamic changes
first, then the stain.
2026-11-13 17:24:06 -08:00
BFlat 31eb3b0d1e Merge pull request #7285 from dotCooCoo/fix/pdf-warped-image-written-once
PDF export: a warped image writes its pixels once for all its pieces
2026-11-14 05:39:34 +07:00
BFlat 4558196e16 Merge pull request #7053 from dotCooCoo/feat/warichu
Type: Warichu stacks selected characters on lines inside their line
2026-11-14 04:32:40 +07:00
dotCooCoo 55f43d2acb PDF export: a warped image writes its pixels once for all its pieces
With downsampling on, each piece of an image bent by a Warp effect was
resampled for its own placed size, so a warp wrote one image XObject for
each size a piece was placed at (four for a 4 x 4 warp that stretches
across, eight for a colorized one with its soft masks). The pieces are
now resampled for the widest and the highest size any piece places the
image at, and all paint that one image. Pieces off the page count too,
and another placement of the same image keeps its own size.

Without downsampling, a colorized image and a CMYK image with a grid
mask no longer key their cache by placed size, so each is decoded and
encoded once for all the sizes it is placed at, as plain images are.
2026-11-13 12:54:12 -08:00
BFlat 9107dad274 Merge pull request #7194 from dotCooCoo/fix/item-3829
Envelopes: a partly transparent image keeps its alpha where its warp pieces overlap
2026-11-14 03:23:28 +07:00
dotCooCoo fdedfb0e59 Merge main into feat/warichu 2026-11-13 11:59:22 -08:00
dotCooCoo 4d4d7ee1a7 Envelopes: opaque pixels of a warped image stay opaque where its pieces overlap
In SVG and PDF output, each piece of a warped image whose pieces can show
through was drawn through a mask of every later piece that reaches it. On a
later piece's antialiased edge the viewer multiplies the earlier piece's
coverage by the mask and draws the later piece over it, so an image with
both opaque and partly transparent pixels came out lighter at its opaque
pixels along the triangles' edges (alpha 184 of 255 in the new SVG test).

A later piece now goes into an earlier piece's mask only when a pixel of
either piece's image within 2 pixels of their overlap is less than opaque
(vectorcraft_effects::WarpSeams). Where two pieces overlap over opaque
pixels, the later one is drawn over the earlier one without a mask.
2026-11-13 10:36:16 -08:00
dotCooCoo 0663c76be1 Envelopes: a partly transparent image keeps its alpha where its warp pieces overlap
An envelope or the Warp effect cuts an image into triangle pieces that
reach a little past their edges, so neighbors overlap and no seams show.
On an image with alpha, the overlap drew that alpha twice: a 50% image
came out about 75% opaque along every cell edge and diagonal.

- The pieces now form a knockout group, and each piece is marked as a
  warp piece (`warp_piece`) whose knockout shape is its whole triangle,
  so where pieces overlap only the top one draws.
- The canvas knocks each piece out by its triangle at full coverage.
- SVG and PDF draw the pieces of a partly transparent image (its pixels,
  opacity, color mask or colorize paint) through one mask each, of the
  later triangles that reach it; pixels on a triangle's antialiased edge
  come out slightly lighter there. Pieces of an opaque image are drawn
  one over another as before.
- SVG writes one copy of the image for pieces cut by opacity masks
  (Preserve Shape Using Transparency) too.
- A cell count past the generated-object budget makes an empty group
  instead of overflowing.

Docs: docs/target-app-parity.md.
2026-11-13 08:38:48 -08:00
dotCooCoo 5f77b65aa2 Gradients: Hue Clockwise and Hue Counterclockwise mix stops around the hue wheel
The Gradient panel's Method menu gets two methods of VectorCraft's own
beside Classic and Perceptual. They mix a linear, radial, conic or
diamond gradient's colors around the hue wheel: hue, saturation and
brightness each change evenly from one stop to the next, and the hue
angle keeps one direction, as the fixed-direction rules of
https://www.w3.org/TR/css-color-4/#hue-interpolation do. Hue Clockwise
lowers the angle (red, magenta, blue), which is clockwise on the color
wheels, and Hue Counterclockwise raises it (red, yellow, green).

Two stops whose hues are less than half a color level apart count as
one hue and keep it, so a color and a tint or shade of it do not turn
the whole wheel. A stop whose RGB numbers are less than half a color
level apart is gray and takes the hue of the stop it mixes with, and
black takes that stop's saturation too.

- `paint.editGradient` and `paint.setFill` take `method: hue-clockwise`
  or `hue-counterclockwise`, in any case, with a space or an underscore
  for the hyphen or without it; other spellings are an error. The native
  file writes `HueClockwise` and `HueCounterclockwise`.
- The canvas and raster exports draw the gradient through stops on its
  curve, within half a color level.
- Reverse turns the method the other way with the stops, and a Reflect
  spread is repeated from the curve's stops, so both keep the colors.
- SVG and PDF write the gradient as an RGB image, CMYK and spot color
  stops included, with the spot warning. EPS writes a shading in process
  colors with a warning for spot stops. A legacy .ai file and the .ai
  editing data get the colors between the stops as stops of their own,
  in process colors, at most 256, with a warning. CSS gets those stops
  too.
- The `method` error names the four values, in every catalog.
- The Method menu is as wide as its longest name in the interface
  language, within the panel.

Docs: docs/mcp.md, docs/plugins.md, docs/target-app-parity.md, ROADMAP.
2026-11-13 08:26:57 -08:00
dotCooCoo 199a9dbce3 Merge main into feat/warichu 2026-11-12 22:28:38 -08:00
dotCooCoo 226ac437b9 Type: Warichu stacks selected characters on lines inside their line
The Character panel menu has Warichu and Warichu Settings… with Show East
Asian Options. Warichu characters are set at a percentage of their size
and stacked on 1 to 6 lines inside their line: one above the other in
horizontal type and side by side in vertical type. Warichu Settings sets
Lines, Line Gap, Scale and Alignment.

- text.setFormat and text.setRangeStyle take warichu, warichuLines,
  warichuGap, warichuScale and warichuAlign; ui.warichuSettings opens
  the dialog.
- A stretch of warichu characters is one block that stays whole on one
  line. The canvas and every export draw the stacked lines; SVG, PDF and
  the .ai writers write such type as outlines, as they do ruby. Type on a
  path sets the characters at their own size.
- The attributes are saved natively, carried by character styles and
  translated in every complete catalog.

Docs: docs/mcp.md, docs/type-parity.md.
2026-11-12 21:13:21 -08:00
dotCooCoo df155042d4 Merge main into feat/image-color-mask 2026-11-12 17:40:13 -08:00
dotCooCoo 15ffb4ec01 Merge main into feat/export-notes-loss-first 2026-11-12 12:45:09 -08:00
dotCooCoo 8d27e53115 Merge main into feat/image-color-mask 2026-11-12 08:56:33 -08:00
dotCooCoo 766e291e3c Save and Export: notes of what the file loses come before notes on options
Each writer marks its notes as a loss (something of the document that the
file leaves out or that reaches it changed) or as a note on the options
(an option it didn't apply or applied another way while the art reaches
the file as it looks, or something an option turns off). The PDF, SVG,
EPS, legacy .ai, DXF and HP-GL writers return their notes on the options
in a list of their own (`setting_notes`) beside `warnings`, and the
engine's `Encoded` does the same.

The replies of the save and export commands, and of
`clipboard.exportPdf` and `clipboard.exportPng`, list the losses first,
each kind in the order the writers gave them, across every file that
Export for Screens writes. The status bar names the first note, so an
export to lossy WebP of type in a font that isn't installed now names the
fallback-font note instead of the lossy WebP note. A print to a file
keeps the order its notes are given in.
2026-11-12 08:33:57 -08:00
dotCooCoo e54519d6bd Merge main into feat/image-color-mask 2026-11-12 07:51:12 -08:00
dotCooCoo 91d705e155 Gradients: repeat or reflect a gradient's stops a set number of times
A linear, radial, conic or diamond gradient has a spread: Pad (the
default, as before), Repeat or Reflect, with a count of 1 to 100
(VectorCraft's own limit). Repeat and Reflect run the stops that many
times along the vector (around the turn for a conic) and keep going past
both ends, Reflect running every other run backwards, as SVG's
spreadMethod does.

The Gradient panel has a Spread menu and a Repeats field, and the
Gradient tool marks where the runs meet. paint.setFill and
paint.editGradient take `spread` and `repeats`; bad values are errors.
Presets, swatches, blends, strokes along or across a path, Outline
Stroke and Object › Expand follow the spread; Expand gives each run the
same whole number of objects, and Outline Stroke's mesh keeps the color
on each side of a sharp step between two stops at one offset.

SVG writes and reads spreadMethod: SVG import now reads a repeating or
reflecting gradient as one that repeats or reflects once, instead of
expanding its stops over the shape. CSS writes repeating gradients.
PDF writes a conic's sweep shading over one run. PDF, EPS, legacy .ai
and the .ai editing data write the other kinds with their stops
repeated across the shape: at most 256 runs and 16,384 stops (256 stops
in .ai files), and for a radial or diamond no farther than 65,536 runs
from its center, with a warning past those. PDF import now finds sharp
steps in any axial or radial shading instead of blurring them.

Docs: docs/mcp.md (Spread), docs/target-app-parity.md, docs/gaps.md.
2026-11-12 06:52:38 -08:00
dotCooCoo 0779da299b Image Tools: Color Mask hides chosen colors of an image while its pixels stay
Object › Plug-ins › Image Tools gains Color Mask… (`addon.image.colorMask`),
Remove Color Mask and Apply Color Mask. Color Mask hides colors of the
selected images without changing their pixels. It matches colors as Remove
Background does: the edge color, the corner color or picked colors, with
Contiguous, Match, Tolerance and Softness. The mask is kept with the image
object and saved with the document: its settings, and a PNG of the image's
pixels with the hidden colors made clear, at the source's depth with its
color profile and resolution. It is a VectorCraft addition.

- The canvas (clip groups and white-shape opacity masks included),
  printing and separations, PDF (soft masks of CMYK images included), SVG,
  EPS, the raster formats, the legacy .ai and .ai editing data, EMF/WMF,
  DXF and the flattener draw the masked pixels. A linked image stays
  linked, and an output that would refer to its file writes the masked
  pixels instead.
- Copy and paste, placing a document and placed documents carry the masked
  pixels with the image, under the names they are given there.
- The dialog opens on the selected image's mask settings, with a Preview.
  Without settings, the command returns them and changes nothing.
- Setting, removing and applying a mask are each one undo step, and no
  step is added when nothing changes. Apply makes the masked pixels the
  image's own and embeds a linked image. A mask that hides nothing is only
  taken off, and the image keeps its pixels and its link.
- When an image's own pixels are replaced (Update Link, Relink, Crop
  Image, a recolor, a linked file read again on open), its mask is made
  again for the new pixels, in every art tree and in opacity-mask art.
  Crop Image cuts the image's own pixels. When the new pixels can't be
  masked, the mask keeps its settings and hides nothing.
- Objects in `ids` that are not images, CMYK images, linked images that
  show their preview, images past the decode allowance, masks that would
  hide every pixel, and grayscale images colorized with a process color
  are refused. The Eyedropper, Object Mosaic, Create Gradient Mesh, Crop
  Image's trim and Image Trace read the image's own pixels.
- An image is selected by a click anywhere in its frame, as before.

Remove Background now also refuses a grayscale image colorized with a
process color. Its result has an alpha channel, and a process color is
refused on a grayscale image with transparency.

Docs: docs/mcp.md (Image Tools add-on › Color Mask),
docs/target-app-parity.md.
2026-11-12 05:17:01 -08:00
Taha-Mahmoodi 3eb9e0271c Merge remote-tracking branch 'origin/main' into fix/import-document-title
# Conflicts:
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/target-app-parity.md
2026-11-12 15:18:59 +04:30
BFlat 4eb0915795 Merge pull request #6781 from dotCooCoo/feat/diamond-gradient
Gradients: diamond gradients run the stops from the center out to a square
2026-11-12 12:40:46 +07:00
dotCooCoo 66d21471ae Gradients: diamond gradients run the stops from the center out to a square
A new gradient kind, `diamond`, runs its stops from the center (the
start) out to a square with a corner at the end of the vector; the
aspect stretches the square across the vector into a rhombus. The
Gradient panel's Type row has a Diamond button, the Gradient tool draws
the square with the aspect handle and center ring, and a gradient
fitted to an object covers its box.

- Model and canvas: the level is |x| + |y| in the square's frame; a
  square with no area (no vector, an aspect of 0, numbers out of range)
  paints the last stop. The canvas draws it as pixels, out to a
  stroke's miters, caps and arrowheads. Transforms keep the vector and
  take the mapped size across it as the aspect.
- Commands: `paint.setFill`, `paint.editGradient` and
  `paint.setGradientGeom` take `kind: "diamond"`; a negative aspect or
  a focal point is an error. A conic or diamond gradient that a plug-in
  returns with a focal point loses the focal point.
- Gradient tool: dragging the aspect handle of a radial or diamond
  gradient whose file gave it a negative aspect now sets a positive
  aspect; only a conic's negative aspect mirrors it.
- Export: PDF, SVG, EPS and legacy .ai write four linear gradients, one
  for each quarter of the square, clipped to the shape (PDF as axial
  shadings). PDF and SVG write an image for translucent stops, a
  dithered fill or a flat square, and for the Perceptual method unless
  PDF writes the stops as spot color shadings. SVG writes a diamond on
  type characters as a radial gradient through the corners, and so
  does CSS. A swatch library saved as CSS gets four corner layers, a
  value for the `background` property (a circle when the swatch is
  turned by an angle that is not a multiple of 90 degrees). The .ai
  editing data holds a radial gradient through the corners. The
  document exports give a warning.
- SVG export: a stroke drawn as an image, as conic wedges or as diamond
  quarters now covers the miters, caps and arrowheads that reach past
  its paint box.
- PDF import reads a function-based shading whose color is the same
  along each square about the origin of its space as a diamond.
- Object > Expand gives nested squares or a mesh of rings.

Docs: docs/mcp.md, docs/target-app-parity.md.
2026-11-11 20:27:48 -08:00
dotCooCoo b403fe230f Merge main into fix/pdfa-14-long-pages 2026-11-11 18:43:27 -08:00
BFlat a118925177 Merge pull request #6668 from storytold/fix/sec-pdf-scan-budget
PDF: note fonts once per walk, place repeated rasters without converting again, check image headers before embedding
2026-11-12 09:23:42 +07:00
BFlat 9a4be17f0c Merge pull request #6680 from dotCooCoo/fix/pdf-points-option-pdfa
PDF: the in-points option leaves PDF/A pages within the standard's size limit
2026-11-12 09:13:17 +07:00
dotCooCooandGert Pellin 1b744eae52 Gradients: conic gradients sweep the stops once around the center
A fourth gradient kind, conic (also accepted as `angle` or `conical`),
sweeps the stops once around the center (the gradient's start),
clockwise on the page from the vector, with a seam where the last stop
meets the first. A negative aspect mirrors the sweep, and reflecting an
object turns its sweep the other way, also for a conic without a
vector or with a flat extent.

- Gradient panel: a Conic button in the Type row; the Aspect Ratio
  field works for conic gradients and keeps a mirrored sweep mirrored.
- Gradient tool: the extent ellipse, aspect handle and center ring of a
  radial gradient, without a focal point.
- Rendering, swatch chips, transforms, warps, plug-in round trips, and
  Object > Expand into wedges or a mesh of columns around the center.
- Commands: a negative `aspect` is an error for linear and radial
  gradients, and so is a `focal` point for a conic gradient.
- PDF export writes a function-based shading whose calculator function
  turns the angle into the ramp, and PDF import reads a function-based
  shading that turns around the origin of its space back as a conic
  gradient, also when that origin lies outside the painted area. A
  PDF/A file, or a conic with more than 97 stops, gets an image
  instead.
- SVG export writes 360 solid wedges clipped to the shape for opaque
  stops and an image otherwise, and a linear gradient on characters.
- CSS export writes `conic-gradient(from ...)`, with the stops mirrored
  for a mirrored sweep.
- EPS and legacy .ai export write 360 solid wedges; the .ai editing data
  holds a linear gradient along the vector with the same stops.
- .af files: conical gradients open as conic gradients.
- An export that writes the gradient in another form gives a warning.

Supersedes #2326 and includes switch87's code from it.

Co-authored-by: Gert Pellin <gert@pellin.be>
2026-11-11 15:51:11 -08:00
dotCooCoo 7bd559cb1d Docs: PDF/A files at PDF 1.4 write long pages in points
A PDF/A file at PDF 1.4 has no UserUnit, so a page longer than 14,400 pt is written in points with or without longPagesInPoints. The param text, docs/mcp.md and the exporter comment now say that only PDF/A files at PDF 1.5 and later keep the UserUnit.
2026-11-11 14:47:18 -08:00
dotCooCoo 2da0dcbba7 PDF: a PDF/A-2b file at PDF 1.4 writes long pages as PDF 1.6 with a UserUnit
PDF/A-2 limits page boundaries to 14,400 units (ISO 19005-2, 6.1.13), and
PDF 1.4 has no UserUnit. A PDF/A-2b export at PDF 1.4 with a page longer
than 14,400 pt was written in points, with a MediaBox over 14,400 units,
whether or not longPagesInPoints was on.

Such a file is now written as PDF 1.6 with a UserUnit and a warning that
names the version, as PDF 1.5 files already are. PDF/A-2 files may declare
PDF 1.0 to 1.7. Pages past 32,767 pt that aren't whole numbers of points
are written too, since the refusal applies only to PDF 1.3 and 1.4 files.
Files whose pages are all 14,400 pt or shorter stay PDF 1.4.
2026-11-11 14:43:24 -08:00
dotCooCoo dc98b59444 PDF: the in-points option leaves PDF/A pages within the standard's size limit
longPagesInPoints now applies only to files without a standard. PDF/A-2 limits page
boundaries to 14,400 units (ISO 19005-2, 6.1.13), so a PDF/A file with a page longer than
200 in keeps its UserUnit whether the option is on or off. The Save PDF dialog disables the
checkbox for PDF/A as it does for PDF/X.

Follows #6643.
2026-11-11 13:45:46 -08:00
Dylan M. Taylor da8c37aa8c PDF: note fonts once per walk, place repeated rasters without converting again, check image headers before embedding 2026-11-11 21:06:08 +00:00
BFlat 541f8beae7 Merge pull request #6643 from dotCooCoo/feat/pdf-large-pages-in-points
PDF: an option writes pages longer than 200 in at full size in points
2026-11-12 02:05:54 +07:00
dotCooCoo 3074560b07 PDF: an option writes pages longer than 200 in at full size in points
The Save PDF dialog's General section gets "Write pages longer than 200 in
at full size in points" (longPagesInPoints in document.exportPdf,
document.export, presets and document.pdfSettings), off by default. When it
is on, a page with a side longer than 14,400 pt is written with its media
box in points and no /UserUnit at every PDF version, and a PDF 1.5 file
stays PDF 1.5. The export's warning for such a page then says it is written
in points. With the option off, and for pages of 14,400 pt or less, the
file is the same as before.

Docs: docs/mcp.md, docs/target-app-parity.md. The new label has a row in
every catalog that lists the dialog's other options.
2026-11-11 10:17:24 -08:00
dotCooCoo 5ac7d97d85 Guides: ruler guides are document objects on their layers
Ruler guides were a list kept beside the layers. Each guide is now an
object of its layer or sublayer, always directly in it and above the
layer's art. These behave as on main:

- Guides draw on screen only. Exports (SVG, PDF, PNG, PSD), Fit All,
  Select All, the art bounds, clicks and marquees, and a layer's
  Pathfinder and Transform effects leave them out. The Layers panel
  lists them as guide rows of their layer, the highest guide.list
  index first.
- Deleting a layer deletes its guides, and merging layers keeps their
  guides in the merged layer.
- The guide commands number guides as guide.list does, in the order
  they were made.

New: each guide takes an id from the document's object ids. An object
command given a guide's id fails with an error, and node.move cannot
put a guide into a group or out of its layers. A guide made while an
opacity mask, a symbol or a pattern is edited moves to the top
visible, unlocked layer when editing ends.

File format: native files still save the guides in the document's
guides list, in every file version, so older builds read them. Each
entry may now also carry the guide's id, name, locked and hidden
flags; older builds ignore those fields. Opening a file puts each
saved guide on its layer (on the top visible, unlocked layer when the
file names none or a layer it does not have), and each saved guide
counts against the object limit for opening a file. A ruler guide node
that a file holds below a layer's art moves up to the layer's guides,
and one anywhere else (in a group or another object, beside the
layers, or in a symbol) is removed. Ruler guides read from an .ai file
become guide objects on the layer they were on, numbered in the order
the file lists them.
2026-11-11 09:52:28 -08:00
Taha-Mahmoodi 92f51896a1 Merge remote-tracking branch 'origin/main' into fix/import-document-title
# Conflicts:
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/target-app-parity.md
2026-11-11 11:16:43 +04:30
BFlat 3cb6f44c9b Merge pull request #6464 from dotCooCoo/fix/issue-6382
Fix #6382: PDF export says how a page longer than 200 inches is written
2026-11-11 04:46:38 +07:00
dotCooCoo a960ba0d08 PDF export: pages that exported before keep exporting
- PDF 1.3 and 1.4 files, PDF/X-1a, PDF/X-3, PDF/X-4 and PDF/A-2b at
  PDF 1.4 among them, write a page longer than 14,400 pt in points
  again, now with a warning that apps that keep to the limit cut it to
  200 in. Only a page that failed with
  "PDF writer error: Limit(TooLargeFloat)" gets the new error: one
  whose size or art needs a real number past 32,767, the most PDF 1.4
  holds. Whole numbers are written as integers, so a page of whole
  points past 32,767 is written as before.
- A page whose side is past 32,767 pt and not a whole number of points
  is refused before the thumbnails are drawn and the transparency is
  flattened.
- A PDF 1.5 file written as PDF 1.6 for a long page keeps PDF 1.5's
  encryption (128-bit RC4), which the Save PDF dialog shows.
- A long page whose short side would be under 3 units at its UserUnit
  (ISO 32000-1, Annex C.2), such as 50 m x 4 mm, gets a smaller
  UserUnit that keeps the short side at 3 units, with a warning.
- The warnings give a page's size rounded up, as the error does.

A page of 14,400 pt or less is written as before. The new warning has
rows in every complete catalog. Docs: docs/mcp.md,
docs/target-app-parity.md and the document.exportPdf params.
2026-11-10 12:31:53 -08:00
BFlat 776e468e89 Merge pull request #6242 from annoybot/fix/pdf-gradient-forms-macos
M4: fix PDF gradient placement in transparency groups
2026-11-10 20:05:34 +07:00
dotCooCoo 2b7b7503eb Fix #6382: PDF export says how a page longer than 200 inches is written
A page with a side longer than 14,400 pt (200 in, ISO 32000-1 Annex C.2)
keeps its size only with a UserUnit, which PDF 1.6 added. Exports now
report such pages and handle the versions without a UserUnit:

- Each such page adds a warning with its size and its UserUnit. Apps
  that ignore UserUnit show the page that many times smaller.
- A PDF 1.5 file with such a page is written as PDF 1.6, with a warning.
- PDF 1.3 and 1.4 files, PDF/X-1a and PDF/X-3 among them, are refused
  with an error that names the page and the limit, instead of
  "PDF writer error: Limit(TooLargeFloat)".
- PDF/X-4 files at PDF 1.5 or 1.6 keep the page in points, with a
  warning that apps that keep to the limit cut it to 200 in.

The warnings and the error have rows in every complete catalog. Docs:
docs/mcp.md, docs/target-app-parity.md and the document.exportPdf
params.
2026-11-09 21:52:13 -08:00
Ahmed Gomaa 9b797c06d1 PDF export: letters with marks are written once in the text layer
A mark glyph's vertical translation started a new draw_glyphs batch, so
krilla received the cluster's source range again and mapped its
characters twice. Copying or searching the exported PDF then found
doubled letters for Arabic harakat in any font, and for every dotted
letter in fonts that draw dots as zero-advance mark glyphs (Noto Sans,
Naskh and Kufi Arabic).

Keep compatible glyphs on the same layout line in one batch and carry
their relative vertical placement as per-glyph y_offset; horizontal
placement is unchanged (next_x - x advances). Font, size, linear
transform, line/column, paint run, list marker and hyphen boundaries
still split batches. Krilla keeps owning cluster mappings and
ActualText. No importer change.

Regressions decode the content stream and ToUnicode maps: one batch per
layout line, no duplicated cluster assignment, one ActualText per
multi-glyph cluster, contours within 0.05 pt of outline export; mixed
scripts, vertical columns, path rotations and PDF/X variants covered.
The reader round-trip stays an ignored test until the importer consumes
ActualText (#6165).

Addresses #6309.
2026-11-09 18:13:14 +02:00
dotCooCoo 90bc821f3f PDF: dithered and Perceptual CMYK gradients stay CMYK
A dithered fill or a Perceptual linear or radial gradient whose stops
are all CMYK was written as an RGB image. It is now a CMYK image.
The stops keep their inks, an off-center midpoint is halfway between
its two stops' inks, and a Perceptual curve uses the ink mixes a
DeviceN shading uses. Dither's noise is added per ink and adds no ink
where the gradient has none.

The Output settings convert the inks as they do for any CMYK image.
A gradient with an RGB, gray or Lab stop, a stop linked to a spot
swatch, or a Perceptual curve of more than 4,096 stops is still written
as an RGB image.
2026-11-09 05:55:04 -08:00
Patrick Nadeau ac674accfd M4: fix PDF gradient placement in transparency groups
Move form transforms into page space so Quartz keeps gradients aligned with their artwork. Add macOS rendering regressions and round-trip coverage for transparency, masks, layers and overprint.

Update the fix for the shared craft-pdf writer and the split exporter modules. Preserve upstream mask, coordinate-limit and layer-box fixes, moving the layer's paper bounds into the form's new space too.

Verified all three Quartz regressions fail on upstream 65adb7b21 and all five gradient-form tests pass with the fix. The new translated-layer regression also fails without moving the layer's paper bounds.

Before the final rebase onto 782f213e4: 463 PDF tests passed (one existing ignored test), workspace fmt, PDF Clippy, wasm, pdf-deps, layers, assets and brands passed. The final rebase reconciles documentation only; the PDF crate is unchanged from that tested revision.

After the final rebase, workspace fmt and Clippy pass. Full cargo xtask ci timed out while compiling workspace tests, so the full test suite and remaining CI stages did not complete. The earlier macOS Clippy blocker has been fixed upstream.
2026-11-09 08:45:23 -05:00
Taha-Mahmoodi cbf5ee51ae Merge remote-tracking branch 'origin/main' into fix/import-document-title
# Conflicts:
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/target-app-parity.md
2026-11-09 11:48:04 +04:30
BFlat 8eb3d4166a Merge pull request #2552 from dotCooCoo/feat/print-copies-per-sheet
Print: Copies per Sheet prints as many copies of each artboard as fit the sheet
2026-11-09 11:17:48 +08:00
dotCooCoo 134520402b PDF/print: import Vec2 in the export writer after merging main 2026-11-08 18:22:08 -08:00
dotCooCoo 967d77919f Print: every copy on a sheet draws the same symbol instances
Each copy on a sheet draws within an equal share of the symbol objects the
export has left (MAX_SYMBOL_NODES), so a document near the limit leaves out
the same instances in every copy instead of dropping them from the later
copies. The limit stays export-wide; one copy per page draws as before.
2026-11-08 18:22:08 -08:00
dotCooCoo b1236c469b Print: Copies per Sheet prints as many copies of each artboard as fit the sheet
File › Print printed each artboard on a sheet of its own. A 2 × 3.5 in
card on Letter printed once in the middle of the page, and Print had no
option to fill the sheet with copies.

The Print dialog's General section has Copies per Sheet (off by
default) and a Gutter. With it on, a sheet holds as many copies of the
artboard as fit the imageable area, each with its bleed and marks,
across then down, gutter pt apart. The card prints 4 × 3 on Letter,
and 3 × 3 with an 18 pt gutter. The copies are not turned, and the
placement places the whole grid as it places one copy. An artboard
that does not fit prints one copy per sheet with a warning, and more
than 1,000 copies on a sheet is an error. Tiling leaves the option
out. The PDF and PostScript writers draw the art and the marks once
per copy, the PDF TrimBox holds every copy, the preview draws each
copy, and print.preview lists them as `copies`. With the option off,
the layout lists no copies and the writers draw the page as before.

Docs: docs/mcp.md (Print, Printing), docs/target-app-parity.md.
2026-11-08 17:22:46 -08:00
dotCooCoo ac0bb3c4ce Gradients: freeform points that share a color model mix in that model
The canvas, the gradient chips and the SVG, PDF, EPS and EMF writers mixed
freeform points in display RGB, while Object > Expand and Create Gradient
Mesh mixed them in the model the points share. Points that are all CMYK,
Gray, Lab or RGB now mix in that model in those places too. Points in
different models mix as before: in display RGB, and on ink planes as the
inks of each point.

This changes how existing art looks:

- CMYK and Lab points. With the default Generic CMYK working space and no
  ink planes, halfway between C100 and K100 the canvas drew (17, 106, 139)
  and now draws (54, 118, 143).
- RGB points in a working RGB space other than sRGB mix in that space.
- On ink planes (a CMYK document that shows transparency), RGB and Lab
  points mix first and then separate.

Gray mixes keep their values, and CMYK and Gray points on ink planes draw
as before. RGB points in an sRGB working space draw as before except on
ink planes.

- PDF and EPS write a gradient whose points are all CMYK, or all Gray in a
  CMYK file, as a CMYK image. PDF's Output settings convert it as they
  convert any CMYK image.
- Darkening applies to the points before they mix.
- With Appearance of Black set to Rich, the canvas mixes a gradient with a
  pure CMYK black point in display RGB, and the point fades from RGB black.
2026-11-08 17:18:43 -08:00
dotCooCoo c94269ff72 Type: Composite Fonts… sets each class of character in a font of its own
Squashed from #6244 onto main.
2026-11-09 03:08:23 +07:00
Taha-Mahmoodi c1e35047e4 Merge remote-tracking branch 'origin/main' into fix/import-document-title
# Conflicts:
#	docs/file-format-parity.md
#	docs/gaps.md
#	docs/target-app-parity.md
2026-11-07 12:40:04 +04:30
dotCooCoo b50a6bcad4 PDF export: the link walk measures only linked objects and clipping groups
The walk that writes link annotations measured every object at every
level, on every export that can have links, and read the box only for
an object with a URL that opens and for a clipping group. It now
measures only those, so a document with no URLs or clipping groups is
not measured a second time. The link annotations are unchanged.

The output is byte for byte the same on a document of linked objects in
plain and clipping groups on and off the page, with default settings,
with uncompressed text and with PDF layers.
2026-11-07 05:15:08 +07:00
dotCooCoo a3045845c8 PDF export: members of a plain group or layer are measured once to leave out off-page art
The walk measured every object's visual box to leave out what is off
the page. A plain group or layer is measured as the union of its
members, and each member then measured itself again when it was
written, so an object in plain groups was measured once for each level
it sits at.

Measuring a plain group or layer now keeps its members' boxes in a
tree, and each member uses its box when it is written. A container
written whatever its box is (a PDF layer's group, placed symbol art) is
not measured. A knockout group's elements are measured for the group's
box and again for the list of elements on the page, and that second box
is used for the element and for its mask. Objects in a live container
(Blend, Envelope, Repeat, Intertwine) are still measured with the
container and again after it is expanded. Node::visual_of_members holds
the formula that Node::visual also uses, so the boxes and every decision
are unchanged.

The output is byte for byte the same on one open path of 200,000
anchors, 20,000 rectangles in 200 groups of 100, and 5,000 point type
objects, and on a document of plain, see-through, clipping and knockout
groups on and off the page, each exported with default settings, with
uncompressed text and with PDF layers.
2026-11-07 05:15:07 +07:00