mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 21:57:04 +00:00
Merge pull request #7808 from dotCooCoo/fix/image-codes-created-inside-a-rolled-back-node
EPS: art left out at the page limit releases the images it encoded
This commit is contained in:
+95
-20
@@ -266,6 +266,14 @@ pub(crate) struct Scene<'a> {
|
||||
images: HashMap<String, Option<usize>>,
|
||||
/// The source key and solid paint together identify a colorized encoding.
|
||||
colorized_images: HashMap<(String, String), Option<usize>>,
|
||||
/// Image keys whose encoding didn't fit under the page limit → how much its first placement
|
||||
/// adds to the body. The image is encoded again only once the page has room for that.
|
||||
too_large_images: HashMap<String, usize>,
|
||||
/// The same for colorized encodings, by `colorized_images`' keys.
|
||||
too_large_colorized: HashMap<(String, String), usize>,
|
||||
/// How many times an image or a colorized image was encoded.
|
||||
#[cfg(test)]
|
||||
encodes: usize,
|
||||
/// Freeform gradients' images by [`ImageCode::content_key`] → their indices in `codes`: a field
|
||||
/// encoded the same as one already kept draws from it.
|
||||
fields: HashMap<u64, Vec<usize>>,
|
||||
@@ -297,6 +305,10 @@ impl<'a> Scene<'a> {
|
||||
brushes: None,
|
||||
images: HashMap::new(),
|
||||
colorized_images: HashMap::new(),
|
||||
too_large_images: HashMap::new(),
|
||||
too_large_colorized: HashMap::new(),
|
||||
#[cfg(test)]
|
||||
encodes: 0,
|
||||
fields: HashMap::new(),
|
||||
codes: vec![],
|
||||
placements: vec![],
|
||||
@@ -313,6 +325,30 @@ impl<'a> Scene<'a> {
|
||||
self
|
||||
}
|
||||
|
||||
/// [`Self::run`], with how many placements draw each encoded image the scene kept, and the
|
||||
/// indices in `codes` that its image, colorized-image and field caches name.
|
||||
#[cfg(test)]
|
||||
pub fn run_with_codes(mut self) -> (Page, Vec<usize>, Vec<usize>) {
|
||||
let doc = self.doc;
|
||||
for l in &doc.layers {
|
||||
self.node(l, false);
|
||||
}
|
||||
let uses = self.codes.iter().map(|c| c.uses).collect();
|
||||
let cached = self.images.values().chain(self.colorized_images.values()).flatten().chain(self.fields.values().flatten()).copied().collect();
|
||||
(self.page(), uses, cached)
|
||||
}
|
||||
|
||||
/// [`Self::run`], with how many times the scene encoded an image or a colorized image.
|
||||
#[cfg(test)]
|
||||
pub fn run_counting_encodes(mut self) -> (Page, usize) {
|
||||
let doc = self.doc;
|
||||
for l in &doc.layers {
|
||||
self.node(l, false);
|
||||
}
|
||||
let encodes = self.encodes;
|
||||
(self.page(), encodes)
|
||||
}
|
||||
|
||||
pub fn run(mut self) -> Page {
|
||||
let doc = self.doc;
|
||||
for l in &doc.layers {
|
||||
@@ -556,7 +592,7 @@ impl<'a> Scene<'a> {
|
||||
if self.size() > self.max_body {
|
||||
self.body.truncate(was);
|
||||
self.unplace(placed);
|
||||
self.forget_fields(codes);
|
||||
self.forget_codes(codes);
|
||||
self.warn(TOO_LARGE);
|
||||
}
|
||||
}
|
||||
@@ -1439,24 +1475,21 @@ impl<'a> Scene<'a> {
|
||||
self.place_image(i, place);
|
||||
}
|
||||
|
||||
/// Forget the field images after the first `n` codes that no placement draws any longer, last
|
||||
/// first, up to the first code that is drawn or isn't a field image.
|
||||
fn forget_fields(&mut self, n: usize) {
|
||||
while self.codes.len() > n {
|
||||
let i = self.codes.len() - 1;
|
||||
let Some(code) = self.codes.get(i) else { break };
|
||||
if code.uses > 0 {
|
||||
break;
|
||||
}
|
||||
let key = code.content_key();
|
||||
let Some(bucket) = self.fields.get_mut(&key) else { break };
|
||||
let Some(at) = bucket.iter().position(|&j| j == i) else { break };
|
||||
bucket.swap_remove(at);
|
||||
if bucket.is_empty() {
|
||||
self.fields.remove(&key);
|
||||
}
|
||||
self.codes.pop();
|
||||
/// Forget the encoded images after the first `n` codes that no placement draws any longer, and
|
||||
/// the cache entries that name them: placed images, colorized images and fields. A code that a
|
||||
/// placement still draws keeps itself and every code before it.
|
||||
fn forget_codes(&mut self, n: usize) {
|
||||
let keep = self.codes.iter().rposition(|c| c.uses > 0).map_or(n, |last| n.max(last + 1));
|
||||
if keep >= self.codes.len() {
|
||||
return;
|
||||
}
|
||||
self.codes.truncate(keep);
|
||||
self.images.retain(|_, e| e.is_none_or(|i| i < keep));
|
||||
self.colorized_images.retain(|_, e| e.is_none_or(|i| i < keep));
|
||||
self.fields.retain(|_, bucket| {
|
||||
bucket.retain(|&i| i < keep);
|
||||
!bucket.is_empty()
|
||||
});
|
||||
}
|
||||
|
||||
/// A placed image (`blob`, cached under `key`): its pixel grid of `width` × `height` placed
|
||||
@@ -1469,7 +1502,28 @@ impl<'a> Scene<'a> {
|
||||
let encoded = match self.images.get(key) {
|
||||
Some(e) => *e,
|
||||
None => {
|
||||
let e = self.decode_image(blob).and_then(|img| self.encode_image(img.as_raw(), img.width(), img.height())).map(|c| self.add_code(c));
|
||||
// An image turned away at the page limit is encoded again only once it fits.
|
||||
if let Some(&len) = self.too_large_images.get(key) {
|
||||
if self.size().saturating_add(len).saturating_add(64) > self.max_body {
|
||||
self.warn(TOO_LARGE);
|
||||
return;
|
||||
}
|
||||
self.too_large_images.remove(key);
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.encodes += 1;
|
||||
}
|
||||
let code = self.decode_image(blob).and_then(|img| self.encode_image(img.as_raw(), img.width(), img.height()));
|
||||
// An image that can't be placed isn't kept.
|
||||
if let Some(c) = &code
|
||||
&& self.size().saturating_add(c.inline_len).saturating_add(64) > self.max_body
|
||||
{
|
||||
self.too_large_images.insert(key.to_string(), c.inline_len);
|
||||
self.warn(TOO_LARGE);
|
||||
return;
|
||||
}
|
||||
let e = code.map(|c| self.add_code(c));
|
||||
self.images.insert(key.to_string(), e);
|
||||
e
|
||||
}
|
||||
@@ -1501,7 +1555,28 @@ impl<'a> Scene<'a> {
|
||||
let encoded = match self.colorized_images.get(&cache) {
|
||||
Some(e) => *e,
|
||||
None => {
|
||||
let e = self.encode_colorized(blob, paint).map(|c| self.add_code(c));
|
||||
// An image turned away at the page limit is encoded again only once it fits.
|
||||
if let Some(&len) = self.too_large_colorized.get(&cache) {
|
||||
if self.size().saturating_add(len).saturating_add(64) > self.max_body {
|
||||
self.warn(TOO_LARGE);
|
||||
return;
|
||||
}
|
||||
self.too_large_colorized.remove(&cache);
|
||||
}
|
||||
#[cfg(test)]
|
||||
{
|
||||
self.encodes += 1;
|
||||
}
|
||||
let code = self.encode_colorized(blob, paint);
|
||||
// An image that can't be placed isn't kept.
|
||||
if let Some(c) = &code
|
||||
&& self.size().saturating_add(c.inline_len).saturating_add(64) > self.max_body
|
||||
{
|
||||
self.too_large_colorized.insert(cache, c.inline_len);
|
||||
self.warn(TOO_LARGE);
|
||||
return;
|
||||
}
|
||||
let e = code.map(|c| self.add_code(c));
|
||||
self.colorized_images.insert(cache, e);
|
||||
e
|
||||
}
|
||||
|
||||
@@ -1495,6 +1495,228 @@ fn a_field_taken_back_at_the_page_limit_leaves_no_image_behind() {
|
||||
assert!(page.setup.contains("/VCim0 VCsrc") && !page.setup.contains("VCim1"), "{}", page.setup);
|
||||
}
|
||||
|
||||
/// A 200 × 200 pt document holding 8 × 8 pixels of noise as image `pic` and a three-pixel gray ramp
|
||||
/// as image `gray`, its first layer holding what `nodes` makes.
|
||||
fn image_doc_with(nodes: impl FnOnce(&mut Document) -> Vec<Node>) -> Document {
|
||||
let mut d = Document::new(200.0, 200.0);
|
||||
let mut png = vec![];
|
||||
noise(8, 8).write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
d.images.insert("pic".into(), ImageBlob::new("image/png", png));
|
||||
let mut gray = vec![];
|
||||
image::DynamicImage::ImageLuma8(image::GrayImage::from_raw(3, 1, vec![0, 128, 255]).unwrap())
|
||||
.write_to(&mut std::io::Cursor::new(&mut gray), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
d.images.insert("gray".into(), ImageBlob::png(gray));
|
||||
let nodes = nodes(&mut d);
|
||||
let layer = d.layers[0].id;
|
||||
for n in nodes {
|
||||
d.insert(Some(layer), usize::MAX, n).unwrap();
|
||||
}
|
||||
d
|
||||
}
|
||||
|
||||
/// One of [`image_doc_with`]'s images: `pic`, or `gray` colorized in process red.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Pic {
|
||||
Placed,
|
||||
Colorized,
|
||||
}
|
||||
|
||||
/// An image of `pic` at (`x`, `y`), its pixels `scale` points wide.
|
||||
fn pic_node(d: &mut Document, pic: Pic, x: f64, y: f64, scale: f64) -> Node {
|
||||
let (key, width, height) = match pic {
|
||||
Pic::Placed => ("pic", 8, 8),
|
||||
Pic::Colorized => ("gray", 3, 1),
|
||||
};
|
||||
let im = ImageObject {
|
||||
key: key.into(),
|
||||
width,
|
||||
height,
|
||||
xf: Affine::translate((x, y)) * Affine::scale(scale),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let mut n = Node::new(d.alloc_id(), NodeKind::Image(Box::new(im)));
|
||||
if let Pic::Colorized = pic {
|
||||
n.extras_mut().colorize = Some(Box::new(Paint::solid(Color::rgb(1.0, 0.0, 0.0))));
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// A pattern whose tile is an image of `pic` about 60 points wide.
|
||||
fn pic_pattern(d: &mut Document, pic: Pic) -> Paint {
|
||||
let scale = match pic {
|
||||
Pic::Placed => 8.0,
|
||||
Pic::Colorized => 20.0,
|
||||
};
|
||||
let tile = pic_node(d, pic, 0.0, 0.0, scale);
|
||||
d.patterns.push(vectorcraft_doc::PatternDef::new("tile", vec![Arc::new(tile)]));
|
||||
Paint::Pattern { pattern: "tile".into(), xf: Affine::IDENTITY }
|
||||
}
|
||||
|
||||
/// A 2000-sided shape filled with each of `fills` in turn, and stroked in black when `stroked`.
|
||||
fn many_sided(d: &mut Document, fills: &[Paint], stroked: bool) -> Node {
|
||||
let stroke = if stroked { Paint::solid(Color::rgb(0.0, 0.0, 0.0)) } else { Paint::None };
|
||||
let mut n = path(d, shapes::polygon(Point::new(100.0, 100.0), 90.0, 2000, 0.0), fills[0].clone(), stroke, 1.0);
|
||||
let Some(AppearanceItem::Fill(first)) = n.appearance.items.first().cloned() else { panic!("a filled shape") };
|
||||
for (k, paint) in fills.iter().enumerate().skip(1) {
|
||||
let mut fill = first.clone();
|
||||
fill.paint = paint.clone();
|
||||
n.appearance.items.insert(k, AppearanceItem::Fill(fill));
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// A page limit that the many-sided shape of `build` (`true`: stroked) passes with its stroke and
|
||||
/// not without it.
|
||||
fn stroke_passes_the_limit(build: impl Fn(bool) -> Document, o: &EpsOptions) -> usize {
|
||||
let fill = crate::scene::Scene::new(&build(false), o).run().body.len();
|
||||
let both = crate::scene::Scene::new(&build(true), o).run().body.len();
|
||||
assert!(both > fill + 20_000, "{fill} {both}");
|
||||
fill + (both - fill) / 2
|
||||
}
|
||||
|
||||
/// Art taken back for passing the page limit after it placed an image (a placed or a colorized
|
||||
/// one) keeps no encoding of that image, and no cache entry names one. A later placement of the
|
||||
/// image is encoded again and written where it is drawn, at both levels.
|
||||
#[test]
|
||||
fn a_rolled_back_node_frees_its_image_codes() {
|
||||
for pic in [Pic::Placed, Pic::Colorized] {
|
||||
for level in Level::ALL {
|
||||
let case = format!("{pic:?}, {level:?}");
|
||||
let build = |stroked: bool, after: bool| {
|
||||
image_doc_with(|d| {
|
||||
let fill = pic_pattern(d, pic);
|
||||
let mut nodes = vec![many_sided(d, &[fill], stroked)];
|
||||
if after {
|
||||
nodes.push(pic_node(d, pic, 20.0, 20.0, 4.0));
|
||||
}
|
||||
nodes
|
||||
})
|
||||
};
|
||||
let o = EpsOptions { level, ..opts(&build(false, false)) };
|
||||
let limit = stroke_passes_the_limit(|stroked| build(stroked, false), &o);
|
||||
let (page, uses, cached) = crate::scene::Scene::new(&build(true, false), &o).with_max_body(limit).run_with_codes();
|
||||
assert!(page.warnings.iter().any(|w| w.contains("too large")), "{case}: {:?}", page.warnings);
|
||||
assert_eq!((uses, cached), (vec![], vec![]), "{case}");
|
||||
assert!(page.body.is_empty() && page.setup.is_empty(), "{case}: {} {}", page.setup, page.body);
|
||||
let (page, uses, cached) = crate::scene::Scene::new(&build(true, true), &o).with_max_body(limit).run_with_codes();
|
||||
assert_eq!((uses, cached), (vec![1], vec![0]), "{case}");
|
||||
let alone = crate::scene::Scene::new(&image_doc_with(|d| vec![pic_node(d, pic, 20.0, 20.0, 4.0)]), &o).run();
|
||||
assert_eq!(alone.body.matches("} exec\n").count(), 1, "{case}: {}", alone.body);
|
||||
assert_eq!((&page.setup, &page.body), (&alone.setup, &alone.body), "{case}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A field encoded before an image in art taken back for passing the page limit is forgotten with
|
||||
/// it. Two later fields sampled the same share one image, the first one the page keeps.
|
||||
#[test]
|
||||
fn a_rolled_back_image_does_not_pin_an_earlier_field_code() {
|
||||
let (red, blue) = (Color::rgb(1.0, 0.0, 0.0), Color::rgb(0.0, 0.0, 1.0));
|
||||
for pic in [Pic::Placed, Pic::Colorized] {
|
||||
for level in Level::ALL {
|
||||
let case = format!("{pic:?}, {level:?}");
|
||||
let build = |stroked: bool, after: bool| {
|
||||
image_doc_with(|d| {
|
||||
let field = freeform(&[(60.0, 100.0, Color::rgb(0.0, 1.0, 0.0)), (140.0, 100.0, red)]);
|
||||
let fills = [field, pic_pattern(d, pic)];
|
||||
let mut nodes = vec![many_sided(d, &fills, stroked)];
|
||||
if after {
|
||||
nodes.extend([freeform_square(d, 0.0, 0.0, 64.0, red, blue), freeform_square(d, 0.0, 0.0, 64.0, red, blue)]);
|
||||
}
|
||||
nodes
|
||||
})
|
||||
};
|
||||
let o = EpsOptions { level, ..opts(&build(false, false)) };
|
||||
let limit = stroke_passes_the_limit(|stroked| build(stroked, false), &o);
|
||||
let (page, uses, cached) = crate::scene::Scene::new(&build(true, false), &o).with_max_body(limit).run_with_codes();
|
||||
assert!(page.warnings.iter().any(|w| w.contains("too large")), "{case}: {:?}", page.warnings);
|
||||
assert_eq!((uses, cached), (vec![], vec![]), "{case}");
|
||||
let (page, uses, cached) = crate::scene::Scene::new(&build(true, true), &o).with_max_body(limit).run_with_codes();
|
||||
assert_eq!((uses, cached), (vec![2], vec![0]), "{case}");
|
||||
assert_eq!(reuses(&page.body, 0, level), 2, "{case}: {}", page.body);
|
||||
let alone = doc_with(|d| vec![freeform_square(d, 0.0, 0.0, 64.0, red, blue), freeform_square(d, 0.0, 0.0, 64.0, red, blue)]);
|
||||
let alone = crate::scene::Scene::new(&alone, &o).run();
|
||||
assert_eq!((&page.setup, &page.body), (&alone.setup, &alone.body), "{case}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An image whose first placement doesn't fit under the page limit is left out with a warning and
|
||||
/// keeps no encoding, though the object that draws it fits. The art before it is written.
|
||||
#[test]
|
||||
fn an_image_rejected_by_the_page_limit_keeps_no_unused_code() {
|
||||
for pic in [Pic::Placed, Pic::Colorized] {
|
||||
for level in Level::ALL {
|
||||
let case = format!("{pic:?}, {level:?}");
|
||||
let d = image_doc_with(|d| vec![red_rect(d), pic_node(d, pic, 20.0, 20.0, 4.0)]);
|
||||
let o = EpsOptions { level, ..opts(&d) };
|
||||
let rect = crate::scene::Scene::new(&image_doc_with(|d| vec![red_rect(d)]), &o).run();
|
||||
let full = crate::scene::Scene::new(&d, &o).run().body.len();
|
||||
let (page, uses, cached) = crate::scene::Scene::new(&d, &o).with_max_body(full - 1).run_with_codes();
|
||||
assert!(page.warnings.iter().any(|w| w.contains("too large")), "{case}: {:?}", page.warnings);
|
||||
assert_eq!((uses, cached), (vec![], vec![]), "{case}");
|
||||
assert_eq!((&page.setup, &page.body), (&rect.setup, &rect.body), "{case}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An image turned away at the page limit is encoded once, not again at each later placement
|
||||
/// that doesn't fit either.
|
||||
#[test]
|
||||
fn an_image_rejected_by_the_page_limit_is_encoded_once() {
|
||||
for pic in [Pic::Placed, Pic::Colorized] {
|
||||
for level in Level::ALL {
|
||||
let case = format!("{pic:?}, {level:?}");
|
||||
let d = image_doc_with(|d| {
|
||||
let mut nodes = vec![red_rect(d)];
|
||||
nodes.extend((0..50).map(|k| pic_node(d, pic, f64::from(k), 20.0, 4.0)));
|
||||
nodes
|
||||
});
|
||||
let o = EpsOptions { level, ..opts(&d) };
|
||||
let rect = crate::scene::Scene::new(&image_doc_with(|d| vec![red_rect(d)]), &o).run();
|
||||
let (page, encodes) = crate::scene::Scene::new(&d, &o).with_max_body(rect.body.len() + 64).run_counting_encodes();
|
||||
assert!(page.warnings.iter().any(|w| w.contains("too large")), "{case}: {:?}", page.warnings);
|
||||
assert_eq!(encodes, 1, "{case}");
|
||||
assert_eq!((&page.setup, &page.body), (&rect.setup, &rect.body), "{case}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An image turned away at the page limit inside art that is then taken back is encoded again
|
||||
/// once the page has room for it, and written where it is drawn.
|
||||
#[test]
|
||||
fn an_image_rejected_in_art_taken_back_is_encoded_again_when_it_fits() {
|
||||
for pic in [Pic::Placed, Pic::Colorized] {
|
||||
for level in Level::ALL {
|
||||
let case = format!("{pic:?}, {level:?}");
|
||||
let build = |stroked: bool, after: bool| {
|
||||
image_doc_with(|d| {
|
||||
let fill = pic_pattern(d, pic);
|
||||
let mut nodes = vec![many_sided(d, &[fill], stroked)];
|
||||
if after {
|
||||
nodes.push(pic_node(d, pic, 20.0, 20.0, 4.0));
|
||||
}
|
||||
nodes
|
||||
})
|
||||
};
|
||||
let o = EpsOptions { level, ..opts(&build(false, false)) };
|
||||
let solid = image_doc_with(|d| vec![many_sided(d, &[Paint::solid(Color::rgb(1.0, 0.0, 0.0))], false)]);
|
||||
// The shape's outline fits under this limit, and its first tile of the image doesn't.
|
||||
let limit = crate::scene::Scene::new(&solid, &o).run().body.len() + 64;
|
||||
let (page, encodes) = crate::scene::Scene::new(&build(false, false), &o).with_max_body(limit).run_counting_encodes();
|
||||
assert!(page.warnings.iter().any(|w| w.contains("too large")), "{case}: {:?}", page.warnings);
|
||||
assert_eq!((encodes, page.body.matches("} exec\n").count()), (1, 0), "{case}: {}", page.body);
|
||||
let (page, encodes) = crate::scene::Scene::new(&build(true, true), &o).with_max_body(limit).run_counting_encodes();
|
||||
assert_eq!(encodes, 2, "{case}");
|
||||
let alone = crate::scene::Scene::new(&image_doc_with(|d| vec![pic_node(d, pic, 20.0, 20.0, 4.0)]), &o).run();
|
||||
assert_eq!((&page.setup, &page.body), (&alone.setup, &alone.body), "{case}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_length_and_ascii85_round_trip() {
|
||||
let cycle: Vec<u8> = (0..=255u8).cycle().take(1000).collect();
|
||||
|
||||
@@ -644,6 +644,7 @@ since 2026-10-01.
|
||||
- **`.ai` files write a warped image's pieces as the parts they show:** in the `.ai` editing data and in legacy `.ai` files, an embedded image cut to a clipping path (in the editing data, also an image under an opacity mask that clips) is written with all of its pixels the first time, and every later copy of the same pixels is written as the rectangle of them that the path or mask shows, 2 pixels more on each side, placed where it lies in the image. The pieces of a Warp effect, an envelope or their expanded art, 200 copies of the image for 10 × 10 cells before, now hold about 7 times the image's pixels in all for a 64 × 64 image. A copy whose opacity mask is left out, nested too deep or past the object limit, keeps all of its pixels. Each image's pixels are read once for the pieces that follow one another. An image cut to a rectangle of half its pixels or more, and the first copy written, keep all of their pixels. The `XI` operator's arguments give an image's location and size ([AI file-format specification, §8.1](https://web.archive.org/web/20050126105313/https://partners.adobe.com/public/developer/en/illustrator/sdk/AI7FileFormat.pdf)); the 2-pixel margin and the half-the-pixels limit are VectorCraft's own rules (eps `ai_editing::tests::the_pieces_of_a_warped_image_are_written_as_the_parts_they_show`, `masked_pieces_of_a_warped_image_are_written_as_the_parts_they_show`, `an_image_with_a_live_warp_writes_its_pixels_whole_once`, `a_clipped_image_written_first_keeps_all_of_its_pixels`, `a_masked_copy_whose_mask_is_left_out_keeps_all_of_its_pixels`, `a_masked_copy_nested_too_deep_for_its_mask_keeps_all_of_its_pixels`, `legacy_ai::tests::the_pieces_of_a_warped_image_are_written_as_the_parts_they_show`). Scores and hours unchanged.
|
||||
- **Legacy `.ai` files work out a colorized warped image's coverage once:** for the pieces of a colorized image cut by a Warp effect, an envelope or their expanded art, a legacy `.ai` save works out how much of the color each pixel shows once for the pieces that follow one another, and no longer decodes the image again for each piece. The file written is unchanged (eps `legacy_ai::tests::the_pieces_of_a_colorized_warped_image_work_out_its_coverage_once`). Scores and hours unchanged.
|
||||
- **EPS writes a repeated freeform gradient's image once:** a freeform gradient whose image samples the same on more than one fill (copies of a shape moved with their points, or one shape in a symbol's instances) has its samples written once in the setup, as a repeated image's are, and a fill whose image differs (other colors, another size) gets its own. A freeform fill drawn once is written where it is drawn, as before, and a fill whose placement cannot be drawn, or that arrives when the page is already at the 256 MB limit, is no longer sampled first (eps `tests::a_freeform_gradients_image_drawn_twice_writes_its_samples_once`, `tests::freeform_gradients_sampled_differently_dont_share_an_image`, `tests::a_field_taken_back_at_the_page_limit_leaves_no_image_behind`). Scores and hours unchanged.
|
||||
- **EPS releases the images of art left out at the page limit:** art taken back because it passes the 256 MB limit on the page no longer keeps the encoded samples of the placed images, colorized images and freeform fills it drew, and an image whose first placement does not fit under the limit is not kept after it is encoded. Later placements of that image are left out with the same warning without encoding it again while it still does not fit; once art taken back leaves room, it is encoded again and written where it is drawn. The page the EPS file draws is the same as before; a repeated image or field written after art taken back can take a lower resource number in the setup (eps `tests::a_rolled_back_node_frees_its_image_codes`, `tests::a_rolled_back_image_does_not_pin_an_earlier_field_code`, `tests::an_image_rejected_by_the_page_limit_keeps_no_unused_code`, `tests::an_image_rejected_by_the_page_limit_is_encoded_once`, `tests::an_image_rejected_in_art_taken_back_is_encoded_again_when_it_fits`). Scores and hours unchanged.
|
||||
- **PDF writes a warped image once:** with downsampling on, the pieces of an image bent by a Warp effect are resampled for the widest and the highest size any piece places the image at, so every piece paints one image XObject (a colorized image: one colorized image and its soft mask), instead of one image for each size a piece is placed at. Another placement of the same image is still resampled for its own size. Without downsampling, a colorized image and a CMYK image with a grayscale image mask on its grid are decoded and encoded once for all the sizes they are placed at (pdf `tests::compression::a_warped_image_is_written_once_for_all_its_pieces`, `tests::colorize::a_colorized_warped_image_is_written_once_for_all_its_pieces`, `export::images::tests::colorized_and_grid_masked_images_are_made_once_for_every_size_without_downsampling`). Scores and hours unchanged.
|
||||
- **Distort Appearance with Transform effect options:** with Envelope Options › Distort Appearance on, a path whose Transform effect has Scale Strokes & Effects or Transform Patterns on, or copies it with a gradient, is bent as the same per-copy art it draws and expands as outside the envelope: each copy's own stroke weight, dashes, pattern placement and gradient (effects `bake::tests`). Scores and hours unchanged.
|
||||
- **Fitted gradients turn with the Transform effect:** a gradient fitted to the object (not placed with the Gradient tool) turns, reflects and stretches with the object when its Transform effect rotates, reflects or scales it unevenly, as Object › Transform already does: the gradient is fixed to its fit on the geometry the effect takes, then follows the effect and the effects after it as a placed gradient does. Under moves and even scales it draws as a refit does, and a Transform effect that scales the object to nothing across leaves it fitted, with Scale Strokes & Effects or Transform Patterns on too, and on each copy. This holds on the canvas and in Object › Expand of the fill, for an object's own effects, a fill's or stroke's own, and a group's, where the gradients fitted to its paths, to its own fill, to the groups inside it and to its images' and symbol instances' own fills each fix to their own object's fit. Existing documents with such a Transform effect on fitted-gradient art draw differently: before, the gradient refit to the turned outline. The Gradient tool still shows a fitted gradient's vector at its unturned fit. How gradients follow the effect is VectorCraft's own rule; what the effect does to the object is described in [Distort and transform effects](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/distort-and-transform-effects-in-illustrator.html) (effects `bake::tests::a_fitted_gradient_turns_reflects_and_stretches_with_a_transform_effect`, `bake::tests::a_transform_effect_that_scales_to_nothing_leaves_a_fitted_gradient_fitted`, `group::tests::a_groups_transform_turns_the_gradients_fitted_to_its_members_and_its_own_fill`; render `objectfx::a_fitted_gradient_turns_and_reflects_with_its_transform_on_the_canvas`; engine `objexpand::a_fitted_gradient_turned_by_a_transform_expands_as_the_canvas_draws_it`). Scores and hours unchanged.
|
||||
|
||||
Reference in New Issue
Block a user