Color Halftone: radii in points for older-version saves; review fixes (#3260)

This commit is contained in:
datatimp
2026-10-30 22:03:13 -04:00
parent b8846ee00f
commit f4f40285b0
4 changed files with 89 additions and 12 deletions
+67 -8
View File
@@ -1,6 +1,7 @@
//! Color Halftone's Max Radius was a distance in points until documents of version 2; since then
//! it is in pixels of the Document Raster Effects resolution, as public documentation gives it
//! (#3260). [`Document::migrate_halftone_radii`] converts the radii of older documents.
//! (#3260). [`Document::migrate_halftone_radii`] converts the radii of older documents, and
//! [`Document::halftone_radii_to_points`] converts them back for a file saved for an older app.
use std::sync::Arc;
@@ -30,14 +31,34 @@ impl Document {
}
self.version = FORMAT_VERSION;
let k = self.raster_ppi() / 72.0;
let fix = &mut |e: &mut Effect| to_pixels(e, k);
self.each_halftone(&mut |e| rescale(e, k));
}
/// For a file saved for an app before version 2 (every native file version before today's):
/// each Color Halftone's Max Radius in pixels becomes the points that draw the same dots,
/// within the 4 to 127 pt that app takes (a radius under 4 pt, from a fine screen at a high
/// resolution, draws bigger dots there), and the document becomes version 1, so reopening the
/// file here converts them back.
pub fn halftone_radii_to_points(&mut self) {
if self.version < PIXEL_RADII_SINCE {
return;
}
self.version = PIXEL_RADII_SINCE - 1;
let k = 72.0 / self.raster_ppi();
self.each_halftone(&mut |e| rescale(e, k));
}
/// `fix` each effect in the art, symbols, patterns (and the one being edited as it was), graph
/// designs, opacity masks and graphic styles that use a Color Halftone.
fn each_halftone(&mut self, fix: &mut dyn FnMut(&mut Effect)) {
for l in &mut self.layers {
node(l, fix);
}
for s in &mut self.symbols {
node(&mut s.art, fix);
}
for p in &mut self.patterns {
// Pattern editing is saved with the pattern as it was, which Cancel puts back.
for p in self.patterns.iter_mut().chain(self.pattern_edit.iter_mut().filter_map(|pe| pe.original.as_mut())) {
for a in &mut p.art {
node(a, fix);
}
@@ -53,8 +74,9 @@ impl Document {
}
}
/// `e`'s radius in points as pixels, `k` pixels a point, when it is a Color Halftone.
fn to_pixels(e: &mut Effect, k: f64) {
/// `e`'s radius times `k` (pixels a point, or points a pixel), within the effect's range, when it
/// is a Color Halftone. Its default is 8 in either unit.
fn rescale(e: &mut Effect, k: f64) {
if e.id != HALFTONE {
return;
}
@@ -109,17 +131,17 @@ mod tests {
fn older_radii_become_the_pixels_that_draw_the_same_dots() {
for (ppi, pt, px) in [(72.0, 8.0, 8.0), (300.0, 8.0, 33.333333333333336), (300.0, 4.0, 16.666666666666668), (150.0, 127.0, 127.0)] {
let mut e = halftone(Some(pt));
to_pixels(&mut e, ppi / 72.0);
rescale(&mut e, ppi / 72.0);
assert!((radius(&e) - px).abs() < 1e-9, "{pt} pt at {ppi} ppi: {}", radius(&e));
assert_eq!(e.params["channel1"], json!(108.0), "the angles stay");
}
// A radius left out drew at the old default, 8 pt.
let mut e = halftone(None);
to_pixels(&mut e, 300.0 / 72.0);
rescale(&mut e, 300.0 / 72.0);
assert!((radius(&e) - 33.333333333333336).abs() < 1e-9);
// Other effects keep their parameters.
let mut blur = Effect { id: "blur.gaussian".into(), params: json!({"radius": 8.0}), visible: true };
to_pixels(&mut blur, 300.0 / 72.0);
rescale(&mut blur, 300.0 / 72.0);
assert_eq!(blur.params, json!({"radius": 8.0}));
}
@@ -142,4 +164,41 @@ mod tests {
d.migrate_halftone_radii();
assert_eq!(d, once, "converted documents are left alone");
}
#[test]
fn radii_go_back_to_points_for_an_older_app_and_return_unchanged() {
let mut d = Document::new(100.0, 100.0);
d.raster_effects_ppi = 300.0;
let mut style = d.graphic_styles.first().cloned().unwrap();
style.appearance.effects.push(halftone(Some(100.0)));
style.appearance.effects.push(halftone(Some(4.0)));
d.graphic_styles.push(style);
let r = |d: &Document, i: usize| d.graphic_styles.last().map(|s| radius(&s.appearance.effects[i])).unwrap();
d.halftone_radii_to_points();
assert_eq!(d.version, 1, "reopened here, it converts back");
assert!((r(&d, 0) - 24.0).abs() < 1e-9, "100 px at 300 ppi is 24 pt: {}", r(&d, 0));
assert_eq!(r(&d, 1), 4.0, "0.96 pt is under the 4 pt the older app takes");
let points = d.clone();
d.halftone_radii_to_points();
assert_eq!(d, points, "already in points");
d.migrate_halftone_radii();
assert!((r(&d, 0) - 100.0).abs() < 1e-9, "{}", r(&d, 0));
}
#[test]
fn the_pattern_being_edited_is_converted_as_it_was() {
let mut d = Document::new(100.0, 100.0);
d.raster_effects_ppi = 300.0;
let id = d.alloc_id();
let mut look = Appearance::default_art();
look.effects = vec![halftone(Some(8.0))];
let square = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(0.0, 0.0, 10.0, 10.0));
let tile = Arc::new(Node::path(id, square, look));
let original = Some(crate::PatternDef::new("Tile", vec![tile]));
d.pattern_edit = Some(crate::PatternEdit { pattern: "Tile".into(), layer: crate::NodeId(0), original });
d.version = 1;
d.migrate_halftone_radii();
let art = &d.pattern_edit.as_ref().unwrap().original.as_ref().unwrap().art[0];
assert!((radius(&art.appearance.effects[0]) - 33.333333333333336).abs() < 1e-9, "the tile Cancel puts back");
}
}
+17
View File
@@ -319,6 +319,10 @@ fn body(doc: &Document, o: &SaveOptions) -> Result<Body, FormatError> {
if version < 3 {
d.store_legacy_color_profiles();
}
// Apps before this one's version take Color Halftone's Max Radius in points (#3260).
if version < VERSION {
d.halftone_radii_to_points();
}
let v1 = if version == 1 {
let mut v = serde_json::to_value(&d).map_err(encode_err)?;
legacy::anchors_to_v1(&mut v);
@@ -804,6 +808,19 @@ mod tests {
let old = load(&save(&d, false)).unwrap();
assert_eq!(old.version, vectorcraft_doc::FORMAT_VERSION);
assert!((radius(&old) - 8.0 * 300.0 / 72.0).abs() < 1e-9, "8 pt at 300 ppi: {}", radius(&old));
// Saved for an older app, in every older version: the radius in points, which opens here
// in pixels again.
let px = radius(&old);
for version in MIN_VERSION..VERSION {
let bytes = save_with(&old, &SaveOptions { version, ..SaveOptions::default() }).unwrap();
let json = String::from_utf8(unpack::Unpacked::new(&bytes).unwrap().json().to_vec()).unwrap();
let at = json.find("\"maxRadius\":").unwrap() + "\"maxRadius\":".len();
let saved: f64 = json[at..].split([',', '}']).next().unwrap().trim().parse().unwrap();
assert!((saved - 8.0).abs() < 1e-9, "version {version} holds 8 pt: {saved}");
let back = load(&bytes).unwrap();
assert!((radius(&back) - px).abs() < 1e-9, "version {version}: {}", radius(&back));
}
assert_eq!(radius(&old), px, "saving leaves the open document as it is");
}
#[test]
+2 -2
View File
@@ -1,6 +1,6 @@
# Effects parity
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-28 · **Change:** minor (Effect dialogs clamp bounded length inputs immediately on commit and flash rejection on invalid input (#4638); previously: Drop Shadow Darkness uses fixed RGB subtraction and CMYK black addition; scores unchanged; Pathfinder › Trap: the Pathfinder panel's Trap… and Effect › Pathfinder › Trap… add an overprinting strip of the lighter colour where two solid colours meet; recounted with Hard Mix (#3061) and Soft Mix already in the catalogue: Pathfinder 13/13, Illustrator effects 49/50) · **Target:** Adobe Illustrator 2026 (30.x)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Pixelate › Color Halftone: Max Radius in pixels of the Document Raster Effects resolution, 4–127 px, with older documents converted on opening and radii written back in points for older app versions (#3260); previously: Effect dialogs clamp bounded length inputs immediately on commit and flash rejection on invalid input (#4638); Drop Shadow Darkness uses fixed RGB subtraction and CMYK black addition; scores unchanged; Pathfinder › Trap: the Pathfinder panel's Trap… and Effect › Pathfinder › Trap… add an overprinting strip of the lighter colour where two solid colours meet; recounted with Hard Mix (#3061) and Soft Mix already in the catalogue: Pathfinder 13/13, Illustrator effects 49/50) · **Target:** Adobe Illustrator 2026 (30.x)
Every entry of Illustrator's Effect menu, by its public feature name, against VectorCraft's effects catalogue
(`effect_catalog()` in `crates/effects/src/lib.rs`) and the Effect menu built from it
@@ -193,7 +193,7 @@ angular cut caps, exact visibility, 3D Classic, Turntable, 3D export) is 25–45
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Pixelate › Color Halftone: Max Radius is in pixels of the Document Raster Effects resolution, 4–127 px, as the [filter effects reference](https://helpx.adobe.com/photoshop/using/filter-effects-reference.html) gives it, instead of points (#3260): fine screens are possible at print resolutions (4 px at 300 ppi ≈ 0.96 pt). The dialog and the Appearance panel show it in px whatever the document units; documents saved before (document version 1) open with each radius converted, so their dots stay the same size. Scores unchanged. Tests: `vectorcraft_effects` `color_halftone_sizes_its_dots_in_pixels_of_the_raster_effects_resolution` and `color_halftones_radius_is_in_raster_pixels_not_a_distance`, `vectorcraft_doc` `halftone_radii::tests`, `vectorcraft_format` `halftone_radii_of_older_documents_open_in_pixels`, `vectorcraft_engine` `a_preserve_editing_pdf_opens_with_its_halftone_radius_in_pixels`, `dialogs::tests::color_halftones_max_radius_stays_in_pixels_whatever_the_units`. |
| 2026-10-30 | minor | Pixelate › Color Halftone: Max Radius is in pixels of the Document Raster Effects resolution, 4–127 px, as the [filter effects reference](https://helpx.adobe.com/photoshop/using/filter-effects-reference.html) gives it, instead of points (#3260): fine screens are possible at print resolutions (4 px at 300 ppi ≈ 0.96 pt). The dialog and the Appearance panel show it in px whatever the document units; documents saved before (document version 1) open with each radius converted, so their dots stay the same size, and a file saved for an older app version (native versions before today's) holds them in points again. Scores unchanged. Tests: `vectorcraft_effects` `color_halftone_sizes_its_dots_in_pixels_of_the_raster_effects_resolution` and `color_halftones_radius_is_in_raster_pixels_not_a_distance`, `vectorcraft_doc` `halftone_radii::tests`, `vectorcraft_format` `halftone_radii_of_older_documents_open_in_pixels`, `vectorcraft_engine` `a_preserve_editing_pdf_opens_with_its_halftone_radius_in_pixels`, `dialogs::tests::color_halftones_max_radius_stays_in_pixels_whatever_the_units`. |
| 2026-10-30 | minor | Effect dialogs: bounded length fields (Color Halftone Max Radius) clamp on commit and visibly reject invalid input (#4638); scores unchanged. |
| 2026-10-23 | minor | Pathfinder › Trap ([trapping](https://helpx.adobe.com/illustrator/using/trapping.html)): the Pathfinder panel's Trap… (`object.pathfinder.trap`, one undo step, the traps grouped above the topmost selected object and selected) and Effect › Pathfinder › Trap… (`pathfinder.trap`, live on groups) add an overprinting strip of the lighter colour inside the darker one where two solid colours meet (Reverse Traps: the darker into the lighter, but not a rich black), with Thickness, Height/Width, Tint Reduction and Traps with Process Color; the effect also has Precision and Remove Redundant Points. Recounted: Hard Mix (#3061) and Soft Mix had shipped, so Pathfinder 11/13 → 13/13 and Illustrator effects 46/50 → 49/50 (SVG Filters remains); remaining 6–10 → 4–8 h. Tests: `vectorcraft_pathops` `trap::tests`, `vectorcraft_effects` `trap::tests` and `group::tests::trap_effect_adds_an_overprinting_strip_above_the_members`, `vectorcraft_engine` `tests_trap`, `panels::pathfinder::tests::trap_opens_its_options_and_ok_traps_the_selection`. |
| 2026-10-22 | minor | The Transform effect has Transform Objects, Transform Patterns and Scale Strokes & Effects, with visible checkboxes (#885); on a fill's or stroke's own Transform effect the last two are still missing. Counts unchanged. |
+3 -2
View File
@@ -1,6 +1,6 @@
# Parity with Adobe Illustrator
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Path Precision Tools add-on: the Parametric Shape Tool; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-30 · **Change:** minor (Effects: Color Halftone's Max Radius in pixels of the Document Raster Effects resolution (#3260); scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026)
## Feature submission evidence
@@ -1129,7 +1129,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
- **Glass's Load Texture:** Effect › Distort › Glass… has a Load Texture… button that reads a PNG, JPEG, GIF, WebP, TIFF, BMP, Targa, PSD, PSB or PCX file and adds Loaded Texture to the Texture menu (`textureImage`: `{path}` or `{name?, dataBase64}`, stored in the effect and saved with the document as a grayscale PNG in base64). The texture lies one image pixel per point at Scaling 100%, centered on the object and repeated; each pixel's luma is the surface's height, from 0 for black to 1 pt for white, and a transparent pixel counts as middle gray. Scaling and Invert act on it as on the built-in textures. A Glass saved without `textureImage` draws and saves as before.
- **Bounded effect length inputs (#4638):** Effect dialog fields with defined length ranges (such as Crystallize Cell Size 3..300 pt) clamp immediately on commit and reject invalid inputs visibly with an accent highlight without reverting to sub-minimum values.
- **Color Halftone in raster pixels (#3260):** Max Radius is 4–127 px of the Document Raster Effects resolution, as public documentation gives it, shown in px whatever the document units; documents from before open with their radii in points converted (document version 2).
- **Color Halftone in raster pixels (#3260):** Max Radius is 4–127 px of the Document Raster Effects resolution, as public documentation gives it, shown in px whatever the document units; documents from before open with their radii in points converted (document version 2), and a file saved for an older app version holds them in points again.
- **Raster effects after document and view changes:** a cached drop shadow, outer glow or filtered content is made again when something its content reads from outside the object changes: a pattern's art or tiling, a symbol shown inside another symbol, a brush definition, an image's pixels, the raster resolution (De-Interlace, Color Halftone), Page Knockout Group, the substitution highlights (Highlight Substituted Fonts and Highlight Alternate Glyphs in Preferences › Type, Highlight Substituted Glyphs in Document Setup), a layer's Dim Images, the color settings, the fonts, Pixel Preview, Anti-aliased Artwork, Overprint Preview and isolation mode. A thumbnail under Color Halftone or Mezzotint is made again when the document's color mode changes. Raster effects (blur, shadow, glow, grain) drawn after a pattern, radial or freeform gradient fill export at their place and size (#1541, fixes #1536).
- **Drop Shadow's Darkness:** Effect › Stylize › Drop Shadow… has Color and Darkness as radio buttons, each with its field, and the Appearance panel's inline editor has the two radio buttons in its Shade row (`shade`: `color` or `darkness`; `darkness`: 0 to 100%, 100 by default). Color is the default, and a shadow saved without these keys draws as before. With Darkness, the stored source-color model determines the adjustment before document/profile conversion and blur: subtract Darkness/100 from each RGB channel, clamped at zero, or add it to CMYK black K and Gray black, clamped at one, preserving CMYK C/M/Y. At the near-100% endpoint the shadow source becomes gray black before conversion. Shadow opacity stays separate. Original gradient stops follow the same application rule before interpolation; exact resource-level gradient parity remains open. Application SVG export keeps a live filter for simple uniform sRGB sources and uses rendered shadow images for other working RGB profiles, source models and gradients. Low-level SVG filter output warns when this preparation is needed; proportional intermediate SVG filters are not misread as additive Darkness. Exact spot/custom-color and composite-source parity remain open, and resulting CMYK ink values follow the application color pipeline. CSS export leaves the shadow out and marks the object unsupported. When a `.ai`, EPS or PDF opens from its editing data, a Drop Shadow with Darkness in an object's art style is read as one with Color is, with the shadow's Darkness percentage (100% when the style gives none). Behavior reference: Illustrator CC Help (2014, [PDF](https://help.adobe.com/archive/en/illustrator/cc/2014/illustrator_reference.pdf)), page 428 (PDF page 434).
@@ -1275,6 +1275,7 @@ spot colours, carry a colour adjustment, or are RGB, CMYK or grayscale images
| Date | Change | Summary |
|---|---|---|
| 2026-10-30 | minor | Effects: Color Halftone's Max Radius is 4–127 px of the Document Raster Effects resolution, shown in px whatever the units; older documents open converted, and files saved for older app versions hold points (#3260); scores unchanged. |
| 2026-10-30 | minor | Path Precision Tools add-on: the Parametric Shape Tool; evidence added; scores unchanged. |
| 2026-10-30 | minor | Presentation Mode fits the active artboard and hides selections, guides, grids and artboard edges; scores and estimates unchanged |
| 2026-10-30 | minor | Color Effects add-on: Select by Color Kind; scores and estimates unchanged |