mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 03:06:57 -06:00
Images: an image past the 2 GiB decode allowance draws at reduced size instead of a crossed box
A PNG, JPEG, GIF or WebP image that takes more than the decode allowance as RGBA (2 GiB, 512 MiB in the web app), such as a 24000 × 24000 JPEG, was placed with the warning "too large to draw" and then drew as a crossed gray box on the canvas and was left out of raster exports. A 16-bit PNG whose decoded samples were past the allowance was shown the same way, with the same warning, although its RGBA fits. The renderer now decodes such an image at the largest size, keeping its proportions, that fits the allowance as RGBA. Each pixel of the result is the mean of the image's pixels it covers. A PNG that isn't interlaced is read a row at a time; other images are decoded whole when their decoded samples fit the allowance. A 16-bit PNG that isn't interlaced and whose RGBA fits is read a row at a time at its own size. An image of more pixels than the allowance has bytes still draws as the crossed box, and so does one whose decoded samples are past the allowance, other than a PNG that isn't interlaced. The renderer decodes an image at reduced size only while the result and the memory reading it takes (the image decoded whole, or the PNG reader's row buffers) fit beside the images being painted; otherwise the image is not decoded on that render (on the canvas it draws as the crossed box) and is tried again on the next render. The document keeps the whole file for saving and export. A placed document's saved preview past the allowance is decoded the same way, at the size it is drawn at or at the reduced size when that is smaller. Open, Place and the Links panel say "an image of W × H pixels is shown at reduced size" for an image drawn smaller. They say nothing for an image drawn at its own size, and they say "too large to draw" for one that is not drawn at reduced size. Docs: docs/mcp.md, docs/target-app-parity.md (the crossed box's line now names an image over the allowance that can't be drawn at reduced size).
This commit is contained in:
Generated
+1
@@ -6214,6 +6214,7 @@ dependencies = [
|
||||
"image",
|
||||
"jpeg-encoder",
|
||||
"log",
|
||||
"png 0.18.1",
|
||||
"proptest",
|
||||
"psd",
|
||||
"serde_json",
|
||||
|
||||
@@ -335,7 +335,7 @@ pub fn load_with(name: &str, bytes: &[u8], opts: &LoadOptions) -> Result<Loaded>
|
||||
if !matches!(format.id, "vectorcraft" | "template") || doc.title.is_empty() {
|
||||
doc.title = title;
|
||||
}
|
||||
warnings.extend(doc.images.values().filter_map(|b| too_large_note(&b.bytes)));
|
||||
warnings.extend(doc.images.values().filter_map(|b| size_note(&b.bytes)));
|
||||
Ok(Loaded { doc, format, warnings, restored, converted, preview_only, editing_data })
|
||||
}
|
||||
|
||||
@@ -515,8 +515,8 @@ pub(crate) use vectorcraft_doc::MAX_RASTER_ALLOC;
|
||||
|
||||
/// What to say of encoded image `bytes` that need more than [`MAX_RASTER_ALLOC`] to decode,
|
||||
/// counted as RGBA (4 bytes a pixel) or as the decoder gives them when that is more (its header
|
||||
/// says so; the pixels aren't read): Open, Place and the Links panel say it (#1479). `None` for
|
||||
/// images within the allowance, and for bytes that aren't an image.
|
||||
/// says so; the pixels aren't read): Open, Place and the Links panel say it ([`size_note`], #1479).
|
||||
/// `None` for images within the allowance, and for bytes that aren't an image.
|
||||
pub(crate) fn too_large_note(bytes: &[u8]) -> Option<String> {
|
||||
use image::ImageDecoder as _;
|
||||
if let Some((w, h)) = vectorcraft_doc::cmyk::oversized_tiff(bytes) {
|
||||
@@ -530,6 +530,17 @@ pub(crate) fn too_large_note(bytes: &[u8]) -> Option<String> {
|
||||
(rgba_over || d.total_bytes() > MAX_RASTER_ALLOC).then(|| format!("an image of {w} × {h} pixels is too large to draw"))
|
||||
}
|
||||
|
||||
/// What Open, Place and the Links panel say of encoded image `bytes` that [`too_large_note`] is
|
||||
/// said of: that the image is shown at reduced size when the renderer decodes it smaller
|
||||
/// ([`vectorcraft_render::reduced_size`]), nothing when the renderer reads it at its own size a
|
||||
/// row at a time, otherwise that note. Returns `None` when [`too_large_note`] returns `None`.
|
||||
pub(crate) fn size_note(bytes: &[u8]) -> Option<String> {
|
||||
let note = too_large_note(bytes)?;
|
||||
let Some(drawn) = vectorcraft_render::reduced_size(bytes) else { return Some(note) };
|
||||
let (w, h) = image::ImageReader::new(Cursor::new(bytes)).with_guessed_format().ok()?.into_dimensions().ok()?;
|
||||
(drawn != (w, h)).then(|| format!("an image of {w} × {h} pixels is shown at reduced size"))
|
||||
}
|
||||
|
||||
/// Decode an image's header (and, for formats stored as PNG, its pixels). CMYK TIFFs are kept as
|
||||
/// they are, with their ink amounts ([`ImageBlob::cmyk`]), except that ones storing one plane per
|
||||
/// ink are written again with their inks together ([`vectorcraft_doc::cmyk::stored_cmyk_tiff`]);
|
||||
|
||||
@@ -32,7 +32,7 @@ mod export;
|
||||
pub(crate) mod imagemap;
|
||||
mod legacy_ai;
|
||||
mod load;
|
||||
pub(crate) use load::{MAX_RASTER_ALLOC, too_large_note};
|
||||
pub(crate) use load::{MAX_RASTER_ALLOC, size_note};
|
||||
mod metafile;
|
||||
mod naming;
|
||||
mod native;
|
||||
|
||||
@@ -185,7 +185,7 @@ fn planar_tiffs_with_more_strips_than_can_be_counted_are_too_large() {
|
||||
file[102..106].copy_from_slice(&1u32.to_le_bytes());
|
||||
let e = raster_image(&file).err().expect("refused");
|
||||
assert!(e.to_string().contains("an image of 3 × 2147483649 pixels is too large to draw"), "{e}");
|
||||
assert!(too_large_note(&file).is_some());
|
||||
assert!(load::too_large_note(&file).is_some());
|
||||
assert_eq!(vectorcraft_doc::cmyk::oversized_tiff(&file), Some((3, 1 << 31 | 1)));
|
||||
}
|
||||
|
||||
|
||||
@@ -76,7 +76,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Links",
|
||||
[],
|
||||
None,
|
||||
"{show?: all (default)|missing|modified|embedded, sort?: name|kind (file format)|status (missing, modified, ok, embedded); default: stacking order; top first} every image object and placed document in the layers, as the Links panel lists them → {links: [{id, name, linked, status: ok|modified|missing|embedded, format, pixelWidth, pixelHeight, path?, found?: path (found away from its path), page?, preview?: true (showing the saved preview), note?: said of an image too large to draw, document?: true (a placed document: pageWidth and pageHeight in pt instead of pixels)}], missing, modified, embedded}",
|
||||
"{show?: all (default)|missing|modified|embedded, sort?: name|kind (file format)|status (missing, modified, ok, embedded); default: stacking order; top first} every image object and placed document in the layers, as the Links panel lists them → {links: [{id, name, linked, status: ok|modified|missing|embedded, format, pixelWidth, pixelHeight, path?, found?: path (found away from its path), page?, preview?: true (showing the saved preview), note?: said of an image past the decode allowance (shown at reduced size, or too large to draw), document?: true (a placed document: pageWidth and pageHeight in pt instead of pixels)}], missing, modified, embedded}",
|
||||
has_doc,
|
||||
list
|
||||
),
|
||||
@@ -1128,7 +1128,7 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Only an image that could be past the decode allowance (at 16 bytes a pixel, the
|
||||
// most a decoder gives) has its header read for this.
|
||||
let large = im.width as u64 * im.height as u64 > fileio::MAX_RASTER_ALLOC / 16;
|
||||
if let Some(note) = st.doc.images.get(&im.key).filter(|_| large).and_then(|b| fileio::too_large_note(&b.bytes)) {
|
||||
if let Some(note) = st.doc.images.get(&im.key).filter(|_| large).and_then(|b| fileio::size_note(&b.bytes)) {
|
||||
row["note"] = json!(note);
|
||||
}
|
||||
(im.link.as_ref(), row)
|
||||
|
||||
@@ -208,7 +208,7 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>, link_pdf: bool) -> Result<Loa
|
||||
let (sx, sy) = pt_per_px(img.ppi);
|
||||
let natural = Rect::new(0.0, 0.0, img.width as f64 * sx, img.height as f64 * sy);
|
||||
let link = src.path.map(|path| super::links::link_info(path, &src.bytes));
|
||||
let warnings = fileio::too_large_note(&img.blob.bytes).into_iter().collect();
|
||||
let warnings = fileio::size_note(&img.blob.bytes).into_iter().collect();
|
||||
return Ok(Loaded { name, format, art: Art::Image(img), natural, warnings, link, board: None });
|
||||
}
|
||||
if pdf {
|
||||
|
||||
@@ -20,8 +20,14 @@ fn place_embedded(s: &mut Session, path: &str) -> NodeId {
|
||||
/// A PNG that says it is `w` × `h` 1-bit grey pixels (its header and no rows, which is all that
|
||||
/// placing and listing read).
|
||||
pub(crate) fn png_header(w: u32, h: u32) -> Vec<u8> {
|
||||
png_header_as(w, h, [1, 0, 0, 0, 0])
|
||||
}
|
||||
|
||||
/// A PNG that says it is `w` × `h` pixels of the bit depth, color type, compression, filter and
|
||||
/// interlace method `kind` gives (its header and no rows).
|
||||
fn png_header_as(w: u32, h: u32, kind: [u8; 5]) -> Vec<u8> {
|
||||
let mut out = b"\x89PNG\r\n\x1a\n".to_vec();
|
||||
let ihdr: Vec<u8> = [&b"IHDR"[..], &w.to_be_bytes(), &h.to_be_bytes(), &[1, 0, 0, 0, 0]].concat();
|
||||
let ihdr: Vec<u8> = [&b"IHDR"[..], &w.to_be_bytes(), &h.to_be_bytes(), &kind].concat();
|
||||
for chunk in [&ihdr[..], b"IDAT\x78\x9c", b"IEND"] {
|
||||
out.extend((chunk.len() as u32 - 4).to_be_bytes());
|
||||
out.extend(chunk);
|
||||
@@ -30,32 +36,42 @@ pub(crate) fn png_header(w: u32, h: u32) -> Vec<u8> {
|
||||
out
|
||||
}
|
||||
|
||||
/// An image past what a decode may take isn't drawn: Place and Open say so and the Links panel's
|
||||
/// row carries the note, where the image used to be blank with no word (#1479).
|
||||
/// An image past what a decode may take is drawn at reduced size: Place and Open say so and the
|
||||
/// Links panel's row carries the note. One the renderer can't read at reduced size (an interlaced
|
||||
/// RGBA PNG past the allowance as decoded, or an image of more pixels than the allowance has
|
||||
/// bytes) isn't drawn, and the note says it is too large to draw (#1479).
|
||||
#[test]
|
||||
fn an_image_too_large_to_draw_is_reported() {
|
||||
const NOTE: &str = "an image of 200000 × 200000 pixels is too large to draw";
|
||||
const NOTE: &str = "an image of 40000 × 40000 pixels is shown at reduced size";
|
||||
const UNDRAWN: &str = "an image of 30000 × 30000 pixels is too large to draw";
|
||||
let dir = Folder::new("huge");
|
||||
let (big, small) = (dir.file("wall.png"), dir.file("small.png"));
|
||||
write(&big, &png_header(200_000, 200_000));
|
||||
let (big, small, laced) = (dir.file("wall.png"), dir.file("small.png"), dir.file("laced.png"));
|
||||
write(&big, &png_header(40_000, 40_000));
|
||||
write(&small, &png(40, 20, BLUE));
|
||||
write(&laced, &png_header_as(30_000, 30_000, [8, 6, 0, 0, 1]));
|
||||
let mut s = session();
|
||||
assert_eq!(run(&mut s, "file.place", json!({"path": big, "at": [200, 150], "link": false}))["warnings"], json!([NOTE]));
|
||||
assert_eq!(run(&mut s, "file.place", json!({"path": small, "at": [20, 20], "link": false}))["warnings"], json!([]));
|
||||
assert_eq!(run(&mut s, "file.place", json!({"path": laced, "at": [20, 20], "link": false}))["warnings"], json!([UNDRAWN]));
|
||||
let l = run(&mut s, "links.list", json!({}));
|
||||
let notes: Vec<&Value> = l["links"].as_array().unwrap().iter().map(|r| &r["note"]).collect();
|
||||
assert_eq!(notes, [&Value::Null, &json!(NOTE)], "top first: the small image has nothing to say");
|
||||
assert_eq!(notes, [&json!(UNDRAWN), &Value::Null, &json!(NOTE)], "top first: the small image has nothing to say");
|
||||
// Opened as a document: said too. An image that decodes (16,000 px square) has no note.
|
||||
assert_eq!(crate::cmd::fileio::load("wall.png", &png_header(200_000, 200_000)).unwrap().warnings, [NOTE]);
|
||||
assert_eq!(crate::cmd::fileio::load("wall.png", &png_header(40_000, 40_000)).unwrap().warnings, [NOTE]);
|
||||
let more = crate::cmd::fileio::load("more.png", &png_header(200_000, 200_000)).unwrap().warnings;
|
||||
assert_eq!(more, ["an image of 200000 × 200000 pixels is too large to draw"], "more pixels than the allowance has bytes");
|
||||
assert!(crate::cmd::fileio::load("fine.png", &png_header(16_000, 16_000)).unwrap().warnings.is_empty());
|
||||
// 20,000 px square at 16 bits a sample is 3.2 GB decoded and 1.6 GB as RGBA: read a row at a
|
||||
// time at its own size, so nothing is said of it.
|
||||
assert!(crate::cmd::fileio::load("deep.png", &png_header_as(20_000, 20_000, [16, 6, 0, 0, 0])).unwrap().warnings.is_empty());
|
||||
}
|
||||
|
||||
/// A gray image is counted as RGBA, 4 bytes a pixel: 23,171 × 23,170 1-bit pixels decode to 537 MB
|
||||
/// of gray and are 2,147,488,280 bytes as RGBA, past the 2 GiB allowance; 23,170 × 23,170 are
|
||||
/// 2,147,395,600 bytes, within it.
|
||||
/// of gray and are 2,147,488,280 bytes as RGBA, past the 2 GiB allowance (shown at reduced size);
|
||||
/// 23,170 × 23,170 are 2,147,395,600 bytes, within it.
|
||||
#[test]
|
||||
fn a_gray_image_too_large_as_rgba_is_reported_on_place() {
|
||||
const NOTE: &str = "an image of 23171 × 23170 pixels is too large to draw";
|
||||
const NOTE: &str = "an image of 23171 × 23170 pixels is shown at reduced size";
|
||||
let dir = Folder::new("huge-gray-place");
|
||||
let (over, within) = (dir.file("over.png"), dir.file("within.png"));
|
||||
write(&over, &png_header(23_171, 23_170));
|
||||
@@ -76,7 +92,7 @@ fn a_gray_image_too_large_as_rgba_is_reported_on_open() {
|
||||
write(&over, &png_header(23_171, 23_170));
|
||||
write(&within, &png_header(23_170, 23_170));
|
||||
let mut s = session();
|
||||
assert_eq!(open(&mut s, &over)["warnings"], json!(["an image of 23171 × 23170 pixels is too large to draw"]));
|
||||
assert_eq!(open(&mut s, &over)["warnings"], json!(["an image of 23171 × 23170 pixels is shown at reduced size"]));
|
||||
assert_eq!(open(&mut s, &within)["warnings"], json!([]));
|
||||
}
|
||||
fn close(a: f64, b: f64) -> bool {
|
||||
|
||||
@@ -24,6 +24,8 @@ jpeg-encoder = { workspace = true }
|
||||
gif = { workspace = true }
|
||||
# TIFF LZW compression (MIT OR Apache-2.0; already used by the image crate's TIFF reader).
|
||||
weezl = "0.2"
|
||||
# PNG rows read one at a time (MIT OR Apache-2.0; already used by the image crate's PNG reader).
|
||||
png = "0.18"
|
||||
log = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
|
||||
@@ -18,7 +18,7 @@ mod ink;
|
||||
mod live;
|
||||
mod lru;
|
||||
mod paint;
|
||||
pub use paint::decode_failures;
|
||||
pub use paint::{decode_failures, reduced_size};
|
||||
mod pattern;
|
||||
pub mod placed_document;
|
||||
pub mod proof;
|
||||
|
||||
+285
-31
@@ -75,15 +75,30 @@ thread_local! {
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// The images [`decode`] has tried to decode on this thread.
|
||||
/// The decode allowance of one image on this thread in place of [`MAX_DECODE_ALLOC`], so a
|
||||
/// test can draw an image past it without decoding gigabytes.
|
||||
pub(crate) static TEST_ALLOWANCE: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
|
||||
/// The bytes [`fits_beside_pinned`] finds room for on this thread, in place of what the images
|
||||
/// paints hold in the shared store leave.
|
||||
pub(crate) static TEST_ROOM: std::cell::Cell<Option<u64>> = const { std::cell::Cell::new(None) };
|
||||
/// The images [`decode_or_reduction`] has tried to decode on this thread.
|
||||
pub(crate) static DECODE_ATTEMPTS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// The most memory decoding one image may take on this thread: [`MAX_DECODE_ALLOC`].
|
||||
fn allowance() -> u64 {
|
||||
#[cfg(test)]
|
||||
if let Some(a) = TEST_ALLOWANCE.with(std::cell::Cell::get) {
|
||||
return a;
|
||||
}
|
||||
MAX_DECODE_ALLOC
|
||||
}
|
||||
|
||||
/// Why an image has no pixels to draw ([`decode_pixmap_or_why`]).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Undecoded {
|
||||
/// Its pixels can't be read, or are more than [`MAX_DECODE_ALLOC`] as RGBA: decoding them
|
||||
/// again fails again.
|
||||
/// Its pixels can't be read, at their own size or at [`reduced_size`]: decoding them again
|
||||
/// fails again.
|
||||
Unreadable,
|
||||
/// The images being painted leave no room for it within twice [`MAX_DECODE_ALLOC`]: it may
|
||||
/// be decoded once they are done.
|
||||
@@ -97,22 +112,6 @@ pub fn decode_failures() -> u64 {
|
||||
DECODE_FAILURES.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// `bytes` decoded within [`MAX_DECODE_ALLOC`] ([`decode_in_room`]); a failure is counted in
|
||||
/// [`decode_failures`].
|
||||
fn decode(bytes: &[u8]) -> Option<image::DynamicImage> {
|
||||
decode_or_why(bytes).ok()
|
||||
}
|
||||
|
||||
/// [`decode`], or why it gave no image (counted in [`decode_failures`] either way).
|
||||
fn decode_or_why(bytes: &[u8]) -> Result<image::DynamicImage, Undecoded> {
|
||||
#[cfg(test)]
|
||||
DECODE_ATTEMPTS.with(|n| n.set(n.get() + 1));
|
||||
decode_in_room(bytes).map_err(|(_, why)| {
|
||||
add_decode_failures(1);
|
||||
why
|
||||
})
|
||||
}
|
||||
|
||||
/// [`decode_in_room`]'s image or error.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn decode_within_allowance(bytes: &[u8]) -> image::ImageResult<image::DynamicImage> {
|
||||
@@ -123,18 +122,17 @@ pub(crate) fn decode_within_allowance(bytes: &[u8]) -> image::ImageResult<image:
|
||||
/// header first, and an image of more than that as RGBA gets a limit error before anything is
|
||||
/// decoded (#1717). A failure comes with why the image gave no pixels ([`room_for`]).
|
||||
fn decode_in_room(bytes: &[u8]) -> Result<image::DynamicImage, (image::ImageError, Undecoded)> {
|
||||
let allowance = allowance();
|
||||
let unreadable = |e| (e, Undecoded::Unreadable);
|
||||
let reader = || -> image::ImageResult<_> {
|
||||
let mut r = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format()?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(MAX_DECODE_ALLOC);
|
||||
limits.max_alloc = Some(allowance);
|
||||
r.limits(limits);
|
||||
Ok(r)
|
||||
};
|
||||
let (w, h) = reader().and_then(|r| r.into_dimensions()).map_err(unreadable)?;
|
||||
let bytes = (u64::from(w) * u64::from(h)).saturating_mul(4);
|
||||
// The images paints hold can't be evicted from the cache, so a document of many images held
|
||||
// them all at once: a new one is decoded only while those and it fit twice the allowance.
|
||||
let pinned = crate::images::store().pinned_bytes() as u64;
|
||||
if let Err(why) = room_for(bytes, pinned) {
|
||||
return Err((image::ImageError::Limits(image::error::LimitError::from_kind(image::error::LimitErrorKind::InsufficientMemory)), why));
|
||||
@@ -143,9 +141,10 @@ fn decode_in_room(bytes: &[u8]) -> Result<image::DynamicImage, (image::ImageErro
|
||||
}
|
||||
|
||||
/// Whether an image of `rgba` bytes may be decoded while the images being painted hold `pinned`
|
||||
/// bytes: within [`MAX_DECODE_ALLOC`] itself, and within twice that with them.
|
||||
/// bytes: within the allowance itself, and within twice [`MAX_DECODE_ALLOC`] with them. An image
|
||||
/// over the allowance may still be decoded at [`reduced_size`].
|
||||
fn room_for(rgba: u64, pinned: u64) -> Result<(), Undecoded> {
|
||||
if rgba > MAX_DECODE_ALLOC {
|
||||
if rgba > allowance() {
|
||||
Err(Undecoded::Unreadable)
|
||||
} else if rgba.saturating_add(pinned) > 2 * MAX_DECODE_ALLOC {
|
||||
Err(Undecoded::NoRoom)
|
||||
@@ -154,6 +153,223 @@ fn room_for(rgba: u64, pinned: u64) -> Result<(), Undecoded> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `bytes` of decoded pixels fit beside the images paints hold, which can't be evicted from
|
||||
/// the cache: a new image is decoded only while those and it fit twice [`MAX_DECODE_ALLOC`].
|
||||
fn fits_beside_pinned(bytes: u64) -> bool {
|
||||
#[cfg(test)]
|
||||
if let Some(room) = TEST_ROOM.with(std::cell::Cell::get) {
|
||||
return bytes <= room;
|
||||
}
|
||||
let pinned = crate::images::store().pinned_bytes() as u64;
|
||||
bytes.saturating_add(pinned) <= 2 * MAX_DECODE_ALLOC
|
||||
}
|
||||
|
||||
/// The size at which [`decode_pixmap_or_why`] decodes encoded image `bytes` that take more than
|
||||
/// [`MAX_DECODE_ALLOC`] as RGBA, or whose decoded samples do (16 bits a sample): the largest that
|
||||
/// fits it as RGBA, keeping its proportions, at most the image's own size, with sides of at most
|
||||
/// `u16::MAX` pixels. `None` for an image within the allowance and for one that isn't decoded at
|
||||
/// all: an image that isn't a PNG, JPEG, GIF or WebP; an image of more pixels than the allowance
|
||||
/// has bytes (2,147,483,648 pixels, or 536,870,912 in the web app; 46,341 × 46,341 is more); and an
|
||||
/// image whose decoded samples take more than the allowance, unless it is a PNG that isn't
|
||||
/// interlaced and is at most the allowance divided by [`ROW_SCRATCH`] pixels wide (read a row at a
|
||||
/// time).
|
||||
pub fn reduced_size(bytes: &[u8]) -> Option<(u32, u32)> {
|
||||
reduction(bytes, allowance()).map(|r| r.to)
|
||||
}
|
||||
|
||||
/// How [`decode_pixmap_or_why`] reads an image past the decode allowance ([`reduced_size`]).
|
||||
struct Reduction {
|
||||
/// The image's size in pixels.
|
||||
size: (u32, u32),
|
||||
/// The size it is decoded at.
|
||||
to: (u32, u32),
|
||||
/// Read a row at a time (a PNG that isn't interlaced) rather than decoded whole first.
|
||||
rows: bool,
|
||||
/// The bytes reading the image takes besides the result: the PNG reader's row buffers, or the
|
||||
/// image decoded whole and a band of its rows as RGBA.
|
||||
scratch: u64,
|
||||
}
|
||||
|
||||
/// [`reduced_size`]'s reading of `bytes` within `allowance`.
|
||||
fn reduction(bytes: &[u8], allowance: u64) -> Option<Reduction> {
|
||||
use image::ImageDecoder as _;
|
||||
let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
|
||||
let format = reader.format()?;
|
||||
if !matches!(format, image::ImageFormat::Png | image::ImageFormat::Jpeg | image::ImageFormat::Gif | image::ImageFormat::WebP) {
|
||||
return None;
|
||||
}
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(allowance);
|
||||
reader.limits(limits);
|
||||
let d = reader.into_decoder().ok()?;
|
||||
let (w, h) = d.dimensions();
|
||||
let pixels = u64::from(w) * u64::from(h);
|
||||
// An image of more pixels than the allowance has bytes (four times what it holds as RGBA) is
|
||||
// not read.
|
||||
if pixels == 0 || pixels > allowance || (pixels.saturating_mul(4) <= allowance && d.total_bytes() <= allowance) {
|
||||
return None;
|
||||
}
|
||||
let to = fitted_dims(w, h, allowance)?;
|
||||
// The PNG reader's row buffers take at most `ROW_SCRATCH` bytes a column, also within the allowance.
|
||||
let rows = format == image::ImageFormat::Png
|
||||
&& u64::from(w).saturating_mul(ROW_SCRATCH) <= allowance
|
||||
&& png_reader(bytes).is_some_and(|r| !r.info().interlaced);
|
||||
let scratch = if rows {
|
||||
u64::from(w) * ROW_SCRATCH
|
||||
} else {
|
||||
let band = u64::from(BAND) * u64::from(w) * (u64::from(d.color_type().bytes_per_pixel()) + 4);
|
||||
d.total_bytes().saturating_add(band)
|
||||
};
|
||||
(rows || d.total_bytes() <= allowance).then_some(Reduction { size: (w, h), to, rows, scratch })
|
||||
}
|
||||
|
||||
/// How many rows of an image decoded whole [`reduced_pixmap`] converts to RGBA at a time.
|
||||
const BAND: u32 = 16;
|
||||
|
||||
/// The most memory a column of a PNG read a row at a time takes in the reader's row buffers: the
|
||||
/// row as stored (up to 8 bytes a pixel and a filter byte), the row before it, and the row given
|
||||
/// out (up to 4 bytes a pixel).
|
||||
const ROW_SCRATCH: u64 = 32;
|
||||
|
||||
/// The largest size of at most `w`×`h` pixels, keeping their proportions, whose pixels take at most
|
||||
/// `allowance` bytes as RGBA, with sides of at most `u16::MAX` pixels.
|
||||
fn fitted_dims(w: u32, h: u32, allowance: u64) -> Option<(u32, u32)> {
|
||||
let pixels = u64::from(w) * u64::from(h);
|
||||
let s = ((allowance / 4) as f64 / pixels as f64).sqrt().min(u16::MAX as f64 / w.max(h) as f64).min(1.0);
|
||||
let side = |n: u32| ((n as f64 * s).floor() as u32).clamp(1, u16::MAX as u32);
|
||||
let (mut nw, mut nh) = (side(w), side(h));
|
||||
// Rounding can leave the product a pixel over: take a pixel off the longer side.
|
||||
while u64::from(nw) * u64::from(nh) * 4 > allowance && (nw > 1 || nh > 1) {
|
||||
if nw >= nh { nw -= 1 } else { nh -= 1 }
|
||||
}
|
||||
(u64::from(nw) * u64::from(nh) * 4 <= allowance).then_some((nw, nh))
|
||||
}
|
||||
|
||||
/// A PNG reader of `bytes` that gives 8-bit gray, gray with alpha, RGB or RGBA rows, with its
|
||||
/// header and the chunks before the pixels read.
|
||||
fn png_reader(bytes: &[u8]) -> Option<png::Reader<std::io::Cursor<&[u8]>>> {
|
||||
let mut d = png::Decoder::new(std::io::Cursor::new(bytes));
|
||||
d.set_transformations(png::Transformations::EXPAND | png::Transformations::STRIP_16);
|
||||
d.read_info().ok()
|
||||
}
|
||||
|
||||
/// `bytes` decoded at the size [`reduction`] gives. Each pixel of the result is the mean of the
|
||||
/// image's pixels it covers. [`Undecoded::Unreadable`] when they can't be read, and
|
||||
/// [`Undecoded::NoRoom`] when the result and what reading takes ([`Reduction::scratch`]) don't fit
|
||||
/// beside the images paints hold.
|
||||
fn reduced_pixmap(bytes: &[u8], r: &Reduction) -> Result<vello_cpu::Pixmap, Undecoded> {
|
||||
let (tw, th) = r.to;
|
||||
if !fits_beside_pinned((u64::from(tw) * u64::from(th) * 4).saturating_add(r.scratch)) {
|
||||
return Err(Undecoded::NoRoom);
|
||||
}
|
||||
shrunk(bytes, r).ok_or(Undecoded::Unreadable)
|
||||
}
|
||||
|
||||
/// `bytes` read and shrunk as [`reduced_pixmap`] says.
|
||||
fn shrunk(bytes: &[u8], r: &Reduction) -> Option<vello_cpu::Pixmap> {
|
||||
let mut shrink = Shrink::new(r.size, r.to);
|
||||
if r.rows {
|
||||
let mut reader = png_reader(bytes)?;
|
||||
let (color, _) = reader.output_color_type();
|
||||
while let Some(row) = reader.next_row().ok()? {
|
||||
let data = row.data();
|
||||
match color {
|
||||
png::ColorType::Grayscale => shrink.row(data.iter().map(|&g| [g, g, g, 255])),
|
||||
png::ColorType::GrayscaleAlpha => shrink.row(data.as_chunks::<2>().0.iter().map(|&[g, a]| [g, g, g, a])),
|
||||
png::ColorType::Rgb => shrink.row(data.as_chunks::<3>().0.iter().map(|&[r, g, b]| [r, g, b, 255])),
|
||||
png::ColorType::Rgba => shrink.row(data.as_chunks::<4>().0.iter().copied()),
|
||||
png::ColorType::Indexed => return None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let mut reader = image::ImageReader::new(std::io::Cursor::new(bytes)).with_guessed_format().ok()?;
|
||||
let mut limits = image::Limits::default();
|
||||
limits.max_alloc = Some(allowance());
|
||||
reader.limits(limits);
|
||||
let img = reader.decode().ok()?;
|
||||
let (w, h) = (img.width(), img.height());
|
||||
for y in (0..h).step_by(BAND as usize) {
|
||||
let band = img.crop_imm(0, y, w, BAND.min(h - y)).to_rgba8();
|
||||
band.as_raw().as_chunks::<4>().0.chunks_exact(w as usize).for_each(|row| shrink.row(row.iter().copied()));
|
||||
}
|
||||
}
|
||||
shrink.finish()
|
||||
}
|
||||
|
||||
/// An image shrunk as its RGBA rows are given ([`Shrink::row`]): each pixel of the result is the
|
||||
/// mean of the image's pixels it covers, premultiplied.
|
||||
struct Shrink {
|
||||
/// The image's size.
|
||||
size: (u32, u32),
|
||||
/// The result's size.
|
||||
to: (u32, u32),
|
||||
/// The sums of the result row being made, a pixel each: red, green and blue times alpha, and
|
||||
/// alpha.
|
||||
sums: Vec<[u64; 4]>,
|
||||
/// How many of the image's pixels each pixel of that row has.
|
||||
counts: Vec<u64>,
|
||||
/// The result row being made, and the next row of the image.
|
||||
at: (u32, u32),
|
||||
/// The finished rows.
|
||||
out: Vec<vello_cpu::color::PremulRgba8>,
|
||||
}
|
||||
|
||||
impl Shrink {
|
||||
fn new(size: (u32, u32), to: (u32, u32)) -> Self {
|
||||
let n = to.0 as usize;
|
||||
Self { size, to, sums: vec![[0; 4]; n], counts: vec![0; n], at: (0, 0), out: Vec::with_capacity(n * to.1 as usize) }
|
||||
}
|
||||
|
||||
/// Add the image's next row of RGBA pixels (pixels past its width are left out).
|
||||
fn row(&mut self, rgba: impl IntoIterator<Item = [u8; 4]>) {
|
||||
if self.at.1 >= self.size.1 {
|
||||
return;
|
||||
}
|
||||
let ty = (u64::from(self.at.1) * u64::from(self.to.1) / u64::from(self.size.1)) as u32;
|
||||
if ty != self.at.0 {
|
||||
self.flush();
|
||||
self.at.0 = ty;
|
||||
}
|
||||
let (w, tw) = (u64::from(self.size.0), u64::from(self.to.0));
|
||||
for (x, p) in (0..w).zip(rgba) {
|
||||
// `tx` is the result column that the image's column `x` falls in.
|
||||
let tx = (x * tw / w) as usize;
|
||||
let a = u64::from(p[3]);
|
||||
if let Some(s) = self.sums.get_mut(tx) {
|
||||
s[0] += u64::from(p[0]) * a;
|
||||
s[1] += u64::from(p[1]) * a;
|
||||
s[2] += u64::from(p[2]) * a;
|
||||
s[3] += a;
|
||||
}
|
||||
if let Some(c) = self.counts.get_mut(tx) {
|
||||
*c += 1;
|
||||
}
|
||||
}
|
||||
self.at.1 += 1;
|
||||
}
|
||||
|
||||
/// Finish the result row being made.
|
||||
fn flush(&mut self) {
|
||||
for (s, n) in self.sums.iter_mut().zip(&mut self.counts) {
|
||||
let k = (*n).max(1);
|
||||
let mean = |v: u64, d: u64| ((v + d / 2) / d).min(255) as u8;
|
||||
self.out.push(vello_cpu::color::PremulRgba8 { r: mean(s[0], 255 * k), g: mean(s[1], 255 * k), b: mean(s[2], 255 * k), a: mean(s[3], k) });
|
||||
*s = [0; 4];
|
||||
*n = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// The shrunk image once every row of the image was given.
|
||||
fn finish(mut self) -> Option<vello_cpu::Pixmap> {
|
||||
if self.at.1 < self.size.1 {
|
||||
return None;
|
||||
}
|
||||
self.flush();
|
||||
let (w, h) = (u16::try_from(self.to.0).ok()?, u16::try_from(self.to.1).ok()?);
|
||||
(self.out.len() == w as usize * h as usize).then(|| vello_cpu::Pixmap::from_parts(self.out, w, h))
|
||||
}
|
||||
}
|
||||
|
||||
/// `img` shrunk, keeping its proportions, to fit a pixmap's sides (at most `u16::MAX` pixels).
|
||||
fn fit_pixmap(img: image::DynamicImage) -> image::DynamicImage {
|
||||
let (w, h) = (img.width(), img.height());
|
||||
@@ -173,14 +389,52 @@ pub fn decode_pixmap(bytes: &[u8]) -> Option<vello_cpu::Pixmap> {
|
||||
}
|
||||
|
||||
/// Decode encoded image bytes into a premultiplied pixmap (shrunk to fit a pixmap's sides), or say
|
||||
/// why there are no pixels.
|
||||
/// why there are no pixels. An image that takes more than the allowance as RGBA is decoded at
|
||||
/// [`reduced_size`] when it has one; a failure is counted in [`decode_failures`].
|
||||
pub(crate) fn decode_pixmap_or_why(bytes: &[u8]) -> Result<vello_cpu::Pixmap, Undecoded> {
|
||||
pixmap_of(fit_pixmap(decode_or_why(bytes)?).to_rgba8()).ok_or(Undecoded::Unreadable)
|
||||
counted(match decode_or_reduction(bytes) {
|
||||
Ok(img) => pixmap_of(fit_pixmap(img).to_rgba8()).ok_or(Undecoded::Unreadable),
|
||||
Err(r) => r.and_then(|r| reduced_pixmap(bytes, &r)),
|
||||
})
|
||||
}
|
||||
|
||||
/// [`decode_pixmap_or_why`] shrunk to `w`×`h` pixels when the image is larger (never enlarged).
|
||||
/// `bytes` decoded within the allowance ([`decode_in_room`]), or, when it refuses them for their
|
||||
/// size, how [`reduced_pixmap`] reads them past it; otherwise why there is no image.
|
||||
fn decode_or_reduction(bytes: &[u8]) -> Result<image::DynamicImage, Result<Reduction, Undecoded>> {
|
||||
#[cfg(test)]
|
||||
DECODE_ATTEMPTS.with(|n| n.set(n.get() + 1));
|
||||
match decode_in_room(bytes) {
|
||||
Ok(img) => Ok(img),
|
||||
Err((image::ImageError::Limits(_), Undecoded::Unreadable)) => Err(reduction(bytes, allowance()).ok_or(Undecoded::Unreadable)),
|
||||
Err((_, why)) => Err(Err(why)),
|
||||
}
|
||||
}
|
||||
|
||||
/// `pm`, with a failure counted in [`decode_failures`] when there is none.
|
||||
fn counted(pm: Result<vello_cpu::Pixmap, Undecoded>) -> Result<vello_cpu::Pixmap, Undecoded> {
|
||||
if pm.is_err() {
|
||||
add_decode_failures(1);
|
||||
}
|
||||
pm
|
||||
}
|
||||
|
||||
/// [`decode_pixmap_or_why`] shrunk to `w`×`h` pixels when the image is larger (never enlarged). An
|
||||
/// image past the decode allowance is decoded straight at `w`×`h` when that is no larger on either
|
||||
/// side and fits the allowance, otherwise at [`reduced_size`].
|
||||
pub fn decode_pixmap_fit(bytes: &[u8], w: u16, h: u16) -> Option<vello_cpu::Pixmap> {
|
||||
let img = decode(bytes)?;
|
||||
let img = match decode_or_reduction(bytes) {
|
||||
Ok(img) => img,
|
||||
Err(r) => {
|
||||
return counted(r.and_then(|mut r| {
|
||||
let fit = (u32::from(w).max(1), u32::from(h).max(1));
|
||||
if fit.0 <= r.to.0 && fit.1 <= r.to.1 {
|
||||
r.to = fit;
|
||||
}
|
||||
reduced_pixmap(bytes, &r)
|
||||
}))
|
||||
.ok();
|
||||
}
|
||||
};
|
||||
let img = if img.width() > w as u32 || img.height() > h as u32 {
|
||||
img.resize_exact(w as u32, h as u32, image::imageops::FilterType::Triangle)
|
||||
} else {
|
||||
@@ -189,9 +443,9 @@ pub fn decode_pixmap_fit(bytes: &[u8], w: u16, h: u16) -> Option<vello_cpu::Pixm
|
||||
pixmap_of(img.to_rgba8())
|
||||
}
|
||||
|
||||
/// Count `n` images that couldn't be decoded on this thread ([`decode_failures`]). [`decode`]
|
||||
/// counts each failure, and a raster kept in a cache counts the failures it was drawn with again
|
||||
/// each time it is drawn.
|
||||
/// Count `n` images that couldn't be decoded on this thread ([`decode_failures`]).
|
||||
/// [`decode_pixmap_or_why`] and [`decode_pixmap_fit`] count each failure, and a raster kept in a cache
|
||||
/// counts the failures it was drawn with again each time it is drawn.
|
||||
pub(crate) fn add_decode_failures(n: u64) {
|
||||
DECODE_FAILURES.with(|c| c.set(c.get() + n));
|
||||
}
|
||||
|
||||
+121
-1
@@ -930,7 +930,9 @@ fn tiff_header(w: u32, h: u32) -> Vec<u8> {
|
||||
|
||||
/// The decode allowance (2 GB) counts an image's pixels as RGBA, read from its header (#1717): a
|
||||
/// 1-bit gray image whose 8-bit gray pixels are within it but whose RGBA is over it is not decoded
|
||||
/// and draws nothing, counted for an export to report; one a column narrower goes on to decoding.
|
||||
/// whole, and one a column narrower goes on to decoding. `decode_pixmap` reads the larger one at
|
||||
/// reduced size; this header has no rows to read, so it draws nothing, counted for an export to
|
||||
/// report.
|
||||
#[test]
|
||||
fn an_image_too_large_as_rgba_is_not_decoded() {
|
||||
let over = paint::decode_within_allowance(&png_header(23_171, 23_170));
|
||||
@@ -1044,6 +1046,124 @@ fn a_gray_image_draws_its_gray_levels() {
|
||||
assert_eq!((r.pixel(5, 10), r.pixel(35, 10)), ([40, 40, 40, 255], [200, 200, 200, 255]));
|
||||
}
|
||||
|
||||
/// Runs `f` with the decode allowance of one image on this thread set to `bytes`.
|
||||
fn with_allowance<T>(bytes: u64, f: impl FnOnce() -> T) -> T {
|
||||
paint::TEST_ALLOWANCE.with(|c| c.set(Some(bytes)));
|
||||
let out = f();
|
||||
paint::TEST_ALLOWANCE.with(|c| c.set(None));
|
||||
out
|
||||
}
|
||||
|
||||
/// `img` encoded as `format`.
|
||||
fn encoded(img: image::DynamicImage, format: image::ImageFormat) -> Vec<u8> {
|
||||
let mut out = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut out), format).unwrap();
|
||||
out
|
||||
}
|
||||
|
||||
/// A 16-bit PNG past the decode allowance is read a row at a time at the largest size within the
|
||||
/// allowance and draws its colors; the file's own size is kept.
|
||||
#[test]
|
||||
fn a_png_past_the_allowance_draws_at_reduced_size() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
|
||||
let img = image::ImageBuffer::<image::Rgba<u16>, _>::from_fn(64, 48, |x, _| {
|
||||
if x < 32 { image::Rgba([51400, 10280, 10280, 65535]) } else { image::Rgba([10280, 10280, 51400, 65535]) }
|
||||
});
|
||||
let png = encoded(image::DynamicImage::ImageRgba16(img), image::ImageFormat::Png);
|
||||
// 64 × 48 is 12,288 bytes as RGBA; 36 × 27 is the largest within 4,096.
|
||||
assert_eq!(with_allowance(4096, || paint::reduced_size(&png)), Some((36, 27)));
|
||||
assert_eq!(paint::reduced_size(&png), None, "within the 2 GB allowance");
|
||||
let mut d = Document::new(64.0, 48.0);
|
||||
d.images.insert("over".into(), ImageBlob::new("image/png", png));
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject { key: "over".into(), width: 64, height: 48, xf: Affine::IDENTITY, link: None, placement: Default::default(), pixels: None };
|
||||
d.insert(Some(d.layers[0].id), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
let failed = decode_failures();
|
||||
// As on screen, where an image that can't be decoded draws as a crossed box.
|
||||
let screen = RenderOptions { smooth_images: false, mark_undecoded_images: true, ..Default::default() };
|
||||
let r = with_allowance(4096, || Renderer::new().render(&d, 64, 48, Affine::IDENTITY, &screen));
|
||||
assert_eq!(decode_failures(), failed);
|
||||
assert_eq!((r.pixel(8, 24), r.pixel(56, 24)), ([200, 40, 40, 255], [40, 40, 200, 255]));
|
||||
// 16 × 16 at 16 bits a sample is 2,048 bytes decoded and 1,024 as RGBA: past 1,500 as decoded
|
||||
// and within it as RGBA, it is read a row at a time at its own size.
|
||||
let small = image::ImageBuffer::<image::Rgba<u16>, _>::from_pixel(16, 16, image::Rgba([51400, 10280, 10280, 65535]));
|
||||
let small = encoded(image::DynamicImage::ImageRgba16(small), image::ImageFormat::Png);
|
||||
assert_eq!(with_allowance(1500, || paint::reduced_size(&small)), Some((16, 16)));
|
||||
let pm = with_allowance(1500, || paint::decode_pixmap(&small)).expect("read a row at a time");
|
||||
let c = pm.sample(3, 3);
|
||||
assert_eq!((pm.width(), pm.height(), [c.r, c.g, c.b, c.a]), (16, 16, [200, 40, 40, 255]));
|
||||
// A row is read only while the reader's row buffers, 32 bytes a column, fit the allowance:
|
||||
// 128 columns do within 4,096 bytes, and 129 columns are not decoded (their samples, 8 bytes a
|
||||
// pixel, are past it too).
|
||||
let wide = |w: u32| {
|
||||
let img = image::ImageBuffer::<image::Rgba<u16>, _>::from_pixel(w, 6, image::Rgba([51400, 10280, 10280, 65535]));
|
||||
encoded(image::DynamicImage::ImageRgba16(img), image::ImageFormat::Png)
|
||||
};
|
||||
assert_eq!(with_allowance(4096, || paint::reduced_size(&wide(128))), Some((128, 6)));
|
||||
assert_eq!(with_allowance(4096, || paint::reduced_size(&wide(129))), None);
|
||||
// An image of more pixels than the allowance has bytes is not read: 128 × 32 8-bit gray pixels
|
||||
// are drawn at 64 × 16 within 4,096 bytes, and 128 × 33 are not.
|
||||
let gray = |h: u32| encoded(image::DynamicImage::ImageLuma8(image::GrayImage::from_pixel(128, h, image::Luma([90]))), image::ImageFormat::Png);
|
||||
assert_eq!(with_allowance(4096, || paint::reduced_size(&gray(32))), Some((64, 16)));
|
||||
assert_eq!(with_allowance(4096, || paint::reduced_size(&gray(33))), None);
|
||||
}
|
||||
|
||||
/// A JPEG whose RGB samples are within the allowance but whose RGBA is past it is decoded whole
|
||||
/// and shrunk to the largest size within the allowance; an image whose decoded samples are past it
|
||||
/// and can't be read a row at a time (a GIF, 4 bytes a pixel) is still not decoded, counted, and
|
||||
/// noted as undecodable.
|
||||
#[test]
|
||||
fn a_jpeg_past_the_allowance_as_rgba_is_shrunk_and_a_gif_past_it_is_not_decoded() {
|
||||
let jpeg = encoded(image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(80, 60, image::Rgb([30, 120, 200]))), image::ImageFormat::Jpeg);
|
||||
// 14,400 bytes of RGB, 19,200 as RGBA: 73 × 54 is the largest within 16,000.
|
||||
let pm = with_allowance(16_000, || paint::decode_pixmap(&jpeg)).expect("decoded at reduced size");
|
||||
assert_eq!((pm.width(), pm.height()), (73, 54));
|
||||
let c = pm.sample(36, 27);
|
||||
assert!(c.r.abs_diff(30) <= 4 && c.g.abs_diff(120) <= 4 && c.b.abs_diff(200) <= 4 && c.a == 255, "{c:?}");
|
||||
// A placed document's preview asks for 20 × 15: decoded straight at that size.
|
||||
let pm = with_allowance(16_000, || paint::decode_pixmap_fit(&jpeg, 20, 15)).expect("decoded at the size asked for");
|
||||
assert_eq!((pm.width(), pm.height()), (20, 15));
|
||||
let gif =
|
||||
encoded(image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(80, 60, image::Rgba([30, 120, 200, 255]))), image::ImageFormat::Gif);
|
||||
assert_eq!(with_allowance(16_000, || paint::reduced_size(&gif)), None);
|
||||
let failed = decode_failures();
|
||||
let why = with_allowance(16_000, || paint::decode_pixmap_or_why(&gif)).err();
|
||||
assert_eq!(why, Some(paint::Undecoded::Unreadable), "noted as undecodable");
|
||||
assert_eq!(decode_failures(), failed + 1);
|
||||
}
|
||||
|
||||
/// An image past the allowance is decoded at reduced size only while the result and what reading
|
||||
/// it takes (the image decoded whole, or the PNG reader's row buffers) fit beside the images
|
||||
/// paints hold; when they don't, it isn't decoded and is not noted as undecodable, and it is
|
||||
/// decoded once there is room.
|
||||
#[test]
|
||||
fn a_reduced_decode_counts_what_reading_takes_beside_the_images_paints_hold() {
|
||||
/// Runs `f` with room for `bytes` beside the images paints hold.
|
||||
fn with_room<T>(bytes: u64, f: impl FnOnce() -> T) -> T {
|
||||
paint::TEST_ROOM.with(|c| c.set(Some(bytes)));
|
||||
let out = f();
|
||||
paint::TEST_ROOM.with(|c| c.set(None));
|
||||
out
|
||||
}
|
||||
// Decoded whole: 14,400 bytes of RGB and a 16-row band of 8,960 bytes, beside a 73 × 54 result
|
||||
// of 15,768 bytes.
|
||||
let jpeg = encoded(image::DynamicImage::ImageRgb8(image::RgbImage::from_pixel(80, 60, image::Rgb([30, 120, 200]))), image::ImageFormat::Jpeg);
|
||||
// Read a row at a time: 64 columns of 32 bytes in the reader's row buffers, beside a 36 × 27
|
||||
// result of 3,888 bytes.
|
||||
let png = encoded(
|
||||
image::DynamicImage::ImageRgba16(image::ImageBuffer::from_pixel(64, 48, image::Rgba([51400, 10280, 10280, 65535]))),
|
||||
image::ImageFormat::Png,
|
||||
);
|
||||
for (bytes, allowance, room, size) in [(&jpeg, 16_000, 15_768 + 14_400 + 8_960, (73, 54)), (&png, 4_096, 3_888 + 2_048, (36, 27))] {
|
||||
let failed = decode_failures();
|
||||
let none = with_allowance(allowance, || with_room(room - 1, || paint::decode_pixmap_or_why(bytes)));
|
||||
assert_eq!(none.err(), Some(paint::Undecoded::NoRoom), "not decoded with a byte too few, and tried again later");
|
||||
assert_eq!(decode_failures(), failed + 1);
|
||||
let pm = with_allowance(allowance, || with_room(room, || paint::decode_pixmap(bytes))).expect("decoded once there is room");
|
||||
assert_eq!((pm.width(), pm.height()), size);
|
||||
}
|
||||
}
|
||||
|
||||
/// Art Optimized exports are supersampled (#983): two red shapes meeting along a diagonal leave no
|
||||
/// seam of the white background between them, where shape-by-shape smoothing shows one; the outer
|
||||
/// edges stay smooth.
|
||||
|
||||
@@ -5127,6 +5127,7 @@ mark-style Japanese Японски
|
||||
theme Appearance Външен вид
|
||||
Load EPS into Clipboard Зареждане на EPS в клипборда
|
||||
@msg an image of {w} × {h} pixels is too large to draw изображение с размери {w} × {h} пиксела е твърде голямо за рисуване
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size изображение с размери {w} × {h} пиксела се показва в намален размер
|
||||
@msg the image can't be read as a texture изображението не може да бъде прочетено като текстура
|
||||
Fill and Stroke Controls Управление на запълването и щриха
|
||||
Coloring Controls Управление на оцветяването
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5239,6 +5239,7 @@ mark-style Japanese Japonské
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} neznámý parametr `{key}`; příkaz přijímá: {keys}
|
||||
Load EPS into Clipboard Načíst EPS do schránky
|
||||
@msg an image of {w} × {h} pixels is too large to draw obraz o velikosti {w} × {h} obrazových bodů je příliš velký na vykreslení
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size obraz o velikosti {w} × {h} obrazových bodů se zobrazuje ve zmenšené velikosti
|
||||
@msg the image can't be read as a texture obraz nelze načíst jako texturu
|
||||
Fill and Stroke Controls Ovládací prvky výplně a tahu
|
||||
Coloring Controls Ovládací prvky barev
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4879,6 +4879,7 @@ mark-style Japanese Japansk
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} ukendt parameter `{key}`; kommandoen tager: {keys}
|
||||
Load EPS into Clipboard Indlæs EPS i udklipsholderen
|
||||
@msg an image of {w} × {h} pixels is too large to draw et billede på {w} × {h} pixels er for stort til at tegne
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size et billede på {w} × {h} pixels vises i reduceret størrelse
|
||||
Fill and Stroke Controls Styring af fyld og streg
|
||||
Coloring Controls Styring af farvelægning
|
||||
Draw Mode Controls Styring af tegnetilstand
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4805,6 +4805,7 @@ lighting Light Licht
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} unbekannter Parameter `{key}`; der Befehl akzeptiert: {keys}
|
||||
Load EPS into Clipboard EPS in die Zwischenablage laden
|
||||
@msg an image of {w} × {h} pixels is too large to draw ein Bild mit {w} × {h} Pixeln ist zu groß, um gezeichnet zu werden
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size ein Bild mit {w} × {h} Pixeln wird verkleinert angezeigt
|
||||
@msg the image can't be read as a texture das Bild kann nicht als Struktur gelesen werden
|
||||
Basic Basis
|
||||
Fill and Stroke Controls Steuerelemente für Fläche und Kontur
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4988,6 +4988,7 @@ effect Points Σημεία
|
||||
Load EPS into Clipboard Φόρτωση EPS στο πρόχειρο
|
||||
@msg unknown control `{name}`: fillStroke, coloring, drawMode or screenMode Άγνωστο στοιχείο ελέγχου `{name}`: fillStroke, coloring, drawMode ή screenMode
|
||||
@msg an image of {w} × {h} pixels is too large to draw Η εικόνα {w} × {h} εικονοστοιχείων είναι υπερβολικά μεγάλη για σχεδίαση
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size Η εικόνα {w} × {h} εικονοστοιχείων εμφανίζεται σε μειωμένο μέγεθος
|
||||
Open Recent File 1 Άνοιγμα πρόσφατου αρχείου 1
|
||||
Open Recent File 2 Άνοιγμα πρόσφατου αρχείου 2
|
||||
Open Recent File 3 Άνοιγμα πρόσφατου αρχείου 3
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2070 and column 19.
|
@@ -5180,6 +5180,7 @@ mark-style Japanese Japonesas
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} parámetro desconocido `{key}`; el comando acepta: {keys}
|
||||
Load EPS into Clipboard Cargar EPS en el portapapeles
|
||||
@msg an image of {w} × {h} pixels is too large to draw una imagen de {w} × {h} píxeles es demasiado grande para dibujarse
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size una imagen de {w} × {h} píxeles se muestra a tamaño reducido
|
||||
@msg the image can't be read as a texture la imagen no se puede leer como textura
|
||||
Fill and Stroke Controls Controles de relleno y trazo
|
||||
Coloring Controls Controles de color
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5128,6 +5128,7 @@ mark-style Japanese Japanilainen
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} tuntematon parametri `{key}`; komennon parametrit: {keys}
|
||||
Load EPS into Clipboard Lataa EPS leikepöydälle
|
||||
@msg an image of {w} × {h} pixels is too large to draw kuva, jonka koko on {w} × {h} pikseliä, on liian suuri piirrettäväksi
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size kuva, jonka koko on {w} × {h} pikseliä, näytetään pienennettynä
|
||||
Fill and Stroke Controls Täytön ja viivan säätimet
|
||||
Coloring Controls Värityksen säätimet
|
||||
Draw Mode Controls Piirtotilan säätimet
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5239,6 +5239,7 @@ mark-style Japanese Japonais
|
||||
theme Appearance Apparence
|
||||
Load EPS into Clipboard Charger un EPS dans le presse-papiers
|
||||
@msg an image of {w} × {h} pixels is too large to draw une image de {w} × {h} pixels est trop grande pour être dessinée
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size une image de {w} × {h} pixels est affichée en taille réduite
|
||||
@msg the image can't be read as a texture l'image ne peut pas être lue comme texture
|
||||
Fill and Stroke Controls Commandes de fond et de contour
|
||||
Coloring Controls Commandes de couleur
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5225,6 +5225,7 @@ imageTrace Outline Körvonal
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} ismeretlen paraméter: `{key}`; a parancs ezeket fogadja el: {keys}
|
||||
Load EPS into Clipboard EPS betöltése a vágólapra
|
||||
@msg an image of {w} × {h} pixels is too large to draw egy {w} × {h} pixeles kép túl nagy a kirajzoláshoz
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size egy {w} × {h} pixeles kép csökkentett méretben jelenik meg
|
||||
@msg the image can't be read as a texture a kép nem olvasható be textúraként
|
||||
Fill and Stroke Controls Kitöltés és körvonal vezérlői
|
||||
Coloring Controls Színezés vezérlői
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4879,6 +4879,7 @@ mark-style Japanese Jepang
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} parameter `{key}` tidak dikenal; perintah menerima: {keys}
|
||||
Load EPS into Clipboard Muat EPS ke papan klip
|
||||
@msg an image of {w} × {h} pixels is too large to draw gambar {w} × {h} piksel terlalu besar untuk digambar
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size gambar {w} × {h} piksel ditampilkan dalam ukuran yang diperkecil
|
||||
Fill and Stroke Controls Kontrol isian dan goresan
|
||||
Coloring Controls Kontrol pewarnaan
|
||||
Draw Mode Controls Kontrol mode menggambar
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 3855 and column 19.
|
@@ -4847,6 +4847,7 @@ lighting Light Luce
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} parametro sconosciuto `{key}`; il comando accetta: {keys}
|
||||
Load EPS into Clipboard Carica EPS negli appunti
|
||||
@msg an image of {w} × {h} pixels is too large to draw un'immagine di {w} × {h} pixel è troppo grande per essere disegnata
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size un'immagine di {w} × {h} pixel viene mostrata a dimensioni ridotte
|
||||
@msg the image can't be read as a texture l'immagine non può essere letta come texture
|
||||
Basic Base
|
||||
Fill and Stroke Controls Controlli di riempimento e traccia
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4886,6 +4886,7 @@ lighting Light ライト
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} `{key}` は不明なパラメーターです。このコマンドで使えるパラメーター: {keys}
|
||||
Load EPS into Clipboard EPS をクリップボードに読み込む
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} ピクセルの画像は大きすぎて描画できません
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} ピクセルの画像は縮小して表示されます
|
||||
@msg the image can't be read as a texture この画像はテクスチャとして読み込めません
|
||||
Basic 基本
|
||||
Fill and Stroke Controls 塗りと線のコントロール
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4800,6 +4800,7 @@ imageTrace Outline კონტური
|
||||
@msg {path}: not a file {path}: ფაილი არ არის
|
||||
Load EPS into Clipboard EPS-ის ბუფერში ჩატვირთვა
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} პიქსელის გამოსახულება დასახატად ზედმეტად დიდია
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} პიქსელის გამოსახულება შემცირებული ზომით ჩანს
|
||||
A later row takes this shortcut ამ მალსახმობს ქვემოთა სტრიქონი იყენებს
|
||||
Add to the Current Shortcuts მიმდინარე მალსახმობებზე დამატება
|
||||
Ambient Light გარემოს სინათლე
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4810,6 +4810,7 @@ lighting Light 광원
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} 알 수 없는 매개변수 `{key}`: 이 명령이 받는 매개변수: {keys}
|
||||
Load EPS into Clipboard EPS를 클립보드로 불러오기
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h}픽셀 이미지는 너무 커서 그릴 수 없습니다
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h}픽셀 이미지는 축소된 크기로 표시됩니다
|
||||
@msg the image can't be read as a texture 이미지를 텍스처로 읽을 수 없습니다
|
||||
Basic 기본
|
||||
Fill and Stroke Controls 칠 및 획 컨트롤
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4891,6 +4891,7 @@ mark-style Japanese Japans
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} onbekende parameter `{key}`; de opdracht accepteert: {keys}
|
||||
Load EPS into Clipboard EPS in het klembord laden
|
||||
@msg an image of {w} × {h} pixels is too large to draw een afbeelding van {w} × {h} pixels is te groot om te tekenen
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size een afbeelding van {w} × {h} pixels wordt verkleind weergegeven
|
||||
Fill and Stroke Controls Besturingselementen voor vulling en lijn
|
||||
Coloring Controls Besturingselementen voor kleuren
|
||||
Draw Mode Controls Besturingselementen voor tekenmodus
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5076,6 +5076,7 @@ mark-style Japanese Japońskie
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} nieznany parametr `{key}`; polecenie przyjmuje: {keys}
|
||||
Load EPS into Clipboard Wczytaj EPS do schowka
|
||||
@msg an image of {w} × {h} pixels is too large to draw obraz o wymiarach {w} × {h} pikseli jest zbyt duży, aby go narysować
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size obraz o wymiarach {w} × {h} pikseli jest wyświetlany w zmniejszonym rozmiarze
|
||||
Basic Podstawowy
|
||||
Fill and Stroke Controls Elementy sterujące wypełnieniem i obrysem
|
||||
Coloring Controls Elementy sterujące kolorem
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5172,6 +5172,7 @@ mark-style Japanese Japonesas
|
||||
@msg The shape is too complex for a 3D effect A forma é complexa demais para um efeito 3D
|
||||
@msg The 3D surface is too complex; simplify the shape A superfície 3D é complexa demais; simplifique a forma
|
||||
@msg an image of {w} × {h} pixels is too large to draw uma imagem de {w} × {h} pixels é grande demais para ser desenhada
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size uma imagem de {w} × {h} pixels é exibida em tamanho reduzido
|
||||
Fill and Stroke Controls Controles de Preenchimento e Traçado
|
||||
Coloring Controls Controles de Coloração
|
||||
Draw Mode Controls Controles de Modo de Desenho
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4843,6 +4843,7 @@ lighting Light Свет
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} неизвестный параметр `{key}`; команда принимает: {keys}
|
||||
Load EPS into Clipboard Загрузить EPS в буфер обмена
|
||||
@msg an image of {w} × {h} pixels is too large to draw изображение размером {w} × {h} пикселей слишком велико для отрисовки
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size изображение размером {w} × {h} пикселей показано в уменьшенном размере
|
||||
@msg the image can't be read as a texture изображение не удаётся прочитать как текстуру
|
||||
Basic Базовый
|
||||
Fill and Stroke Controls Элементы управления заливкой и обводкой
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5144,6 +5144,7 @@ mark-style Japanese Japanska
|
||||
theme Appearance Utseende
|
||||
Load EPS into Clipboard Läs in EPS till urklipp
|
||||
@msg an image of {w} × {h} pixels is too large to draw en bild på {w} × {h} pixlar är för stor för att ritas
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size en bild på {w} × {h} pixlar visas i förminskad storlek
|
||||
Fill and Stroke Controls Kontroller för fyllning och kontur
|
||||
Coloring Controls Kontroller för färgläggning
|
||||
Draw Mode Controls Kontroller för ritläge
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4950,6 +4950,7 @@ mark-style Japanese Japon
|
||||
theme Appearance Görünüm
|
||||
Load EPS into Clipboard EPS'yi Panoya Yükle
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} piksellik bir görüntü çizilemeyecek kadar büyük
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} piksellik bir görüntü küçültülmüş boyutta gösteriliyor
|
||||
@msg the image can't be read as a texture görüntü doku olarak okunamıyor
|
||||
Fill and Stroke Controls Dolgu ve Kontur Kontrolleri
|
||||
Coloring Controls Renklendirme Kontrolleri
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4827,6 +4827,7 @@ lighting Light Світло
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} невідомий параметр `{key}`; команда приймає: {keys}
|
||||
Load EPS into Clipboard Завантажити EPS у буфер обміну
|
||||
@msg an image of {w} × {h} pixels is too large to draw зображення розміром {w} × {h} пікселів завелике для відображення
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size зображення розміром {w} × {h} пікселів показано в зменшеному розмірі
|
||||
@msg the image can't be read as a texture зображення не вдається прочитати як текстуру
|
||||
Basic Базовий
|
||||
Fill and Stroke Controls Елементи керування заливкою та обведенням
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 749 and column 40.
|
@@ -4830,6 +4830,7 @@ theme Appearance Giao diện
|
||||
@msg {tool} isn't in a toolbar group of several tools {tool} không nằm trong nhóm thanh công cụ có nhiều công cụ
|
||||
Load EPS into Clipboard Tải EPS vào bảng tạm
|
||||
@msg an image of {w} × {h} pixels is too large to draw ảnh {w} × {h} điểm ảnh quá lớn nên không thể vẽ
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size ảnh {w} × {h} điểm ảnh được hiển thị ở kích thước thu nhỏ
|
||||
Basic Cơ bản
|
||||
Fill and Stroke Controls Điều khiển tô và nét
|
||||
Coloring Controls Điều khiển màu
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4069 and column 10.
|
@@ -5108,6 +5108,7 @@ lighting Light 光源
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} 未知参数 `{key}`;该命令接受:{keys}
|
||||
Load EPS into Clipboard 将 EPS 载入剪贴板
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} 像素的图像过大,无法绘制
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} 像素的图像以缩小的尺寸显示
|
||||
Fill and Stroke Controls 填色和描边控件
|
||||
Coloring Controls 颜色控件
|
||||
Draw Mode Controls 绘图模式控件
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4919,6 +4919,7 @@ lighting Light 光源
|
||||
@msg unknown parameter `{key}`; the command takes: {keys} 未知的參數 `{key}`;此指令接受:{keys}
|
||||
Load EPS into Clipboard 將 EPS 載入剪貼簿
|
||||
@msg an image of {w} × {h} pixels is too large to draw {w} × {h} 像素的影像過大,無法繪製
|
||||
@msg an image of {w} × {h} pixels is shown at reduced size {w} × {h} 像素的影像以縮小的尺寸顯示
|
||||
Basic 基本
|
||||
Fill and Stroke Controls 填色和筆畫控制項
|
||||
Coloring Controls 顏色控制項
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -265,7 +265,7 @@ fn status_tip(r: &Value) -> String {
|
||||
"embedded" => tl!("Embedded").into(),
|
||||
_ => path.to_string(),
|
||||
};
|
||||
// An image too large to draw (`links.list`'s note).
|
||||
// An image shown at reduced size or too large to draw (`links.list`'s note).
|
||||
match r["note"].as_str() {
|
||||
Some(note) => format!("{tip}\n{}", crate::i18n::msg(note)),
|
||||
None => tip,
|
||||
|
||||
+4
-2
@@ -3599,8 +3599,10 @@ listed in `warnings`. Binary DXF and DWG files answer an error saying to save th
|
||||
|
||||
`links.list {show?: all|missing|modified|embedded, sort?: name|kind|status}` lists every image in the layers, top
|
||||
first, as the Links panel does: `{id, name, linked, status: ok|modified|missing|embedded, format, pixelWidth,
|
||||
pixelHeight, path?, found?, page?, preview?, note?}` (`note`: the image is too large to decode and isn't drawn;
|
||||
`document.open` and `file.place` say the same in their `warnings`). `links.info {id?}` is the Link Info: `image.info`'s fields plus
|
||||
pixelHeight, path?, found?, page?, preview?, note?}` (`note`: the image takes more than the decode allowance, 2 GiB
|
||||
or 512 MiB in the web app, as RGBA or as decoded, and it is drawn at the largest size within the allowance ("shown at
|
||||
reduced size"; the document keeps the whole file) or, when it can't be read that way or has more pixels than the
|
||||
allowance has bytes, drawn as a crossed gray box on the canvas and left out of exports ("too large to draw"); `document.open` and `file.place` say the same in their `warnings`). `links.info {id?}` is the Link Info: `image.info`'s fields plus
|
||||
`status`, `format`, the file's `ppi` and the `effectivePpi`, `scale` (% of 100%), `rotation` (degrees,
|
||||
counter-clockwise), `placement` and, for a linked file, `fileName`, `location`, `fileSize`, `created`, `modified`.
|
||||
`links.goTo {id}` selects an image; `links.embed {ids?}` keeps linked files' pixels in the document (an image whose
|
||||
|
||||
@@ -988,7 +988,8 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
actual OS display and Caps Lock remain unverified. Behaviour reference: [public tool-pointer documentation](https://helpx.adobe.com/illustrator/using/tools.html#change_tool_pointers). Tool cursors are painted into the window on Wayland (#1652; not run on Linux, #1645 stays open until confirmed). A Precise Cursors command toggles the preference and can take a shortcut (#1663). On macOS the tool cursor is painted only while the window has the focus. Over a window behind another app, or under the File › Open… sheet, the system pointer shows alone instead of next to the painted cursor (#2020).
|
||||
In a rotated view the resize and rotate cursors turn with the view, to the nearest 45°.
|
||||
- **Performance:** 20k shapes + 1k texts render in 27 ms per full-retina frame (7.8 ms zoomed), 7× faster than the first version. The UI thread never blocks. The web build is 7.1 MB gzipped. Print-size images (more than 512 MB decoded, up to 2 GB) draw in the canvas and in raster exports and can be trimmed with Crop Image; an image with a side past 65,535 px is shrunk to fit, a raster export says when an image could not be decoded, and Crop Image's trim leaves CMYK images alone (#1517). On Windows `vectorcraft-cli perf` reports the cores busy and, with `--deep`, its working set, and warns on a busy machine as it does on macOS and Linux (#1502).
|
||||
An image whose pixels can't be decoded, such as a PNG cut short or an image over the 2 GB decoding allowance as RGBA, is tried once: the renderer notes it and does not try to decode it again on later frames. An image refused only because the images being drawn leave no room for it is tried again on the next render. On the canvas such an image draws as a light gray box crossed from corner to corner, and the status bar says once for each image that it couldn't be decoded; exports leave it out and report it as before.
|
||||
An image whose pixels can't be decoded, such as a PNG cut short or an image over the 2 GB decoding allowance as RGBA that can't be drawn at reduced size, is tried once: the renderer notes it and does not try to decode it again on later frames. An image refused only because the images being drawn leave no room for it is tried again on the next render. On the canvas such an image draws as a light gray box crossed from corner to corner, and the status bar says once for each image that it couldn't be decoded; exports leave it out and report it as before.
|
||||
A PNG, JPEG, GIF or WebP image that takes more than 2 GiB as RGBA (512 MiB in the web app) draws in the canvas and in raster exports at the largest size within that allowance, instead of as that crossed box on the canvas and not at all in exports. Each pixel of the result is the mean of the image's pixels it covers, and the document keeps the whole file. A PNG that isn't interlaced is read a row at a time, and other images are read whole when their decoded samples fit in the allowance. A 16-bit PNG that isn't interlaced, whose decoded samples take more than the allowance and whose RGBA fits in it, draws at its own size, read a row at a time. An image of more pixels than the allowance has bytes (on the desktop, 46,341 × 46,341 is more) still draws as the crossed box, and so does one whose decoded samples take more than the allowance, other than a PNG that isn't interlaced. Open, Place and the Links panel say "an image of W × H pixels is shown at reduced size", and say "too large to draw" of an image that is not drawn at reduced size.
|
||||
With `--deep`, each memory row of `vectorcraft-cli perf` also gives the peak so far: the peak working set on Windows and the peak resident memory (`VmHWM`) on Linux. A new last row gives the memory after the SVG export and the save → open → save at the end of the run.
|
||||
The renderer composes the two ink planes of a CMYK document that shows transparency into screen colors on up to as many threads as it rasterizes with, instead of on one. At 2880 × 1800 on 8 threads, a page of 300 shapes at 20% to 90% opacity takes 30 ms a frame instead of 55 ms, and 50,000 paths with one at 99% opacity take 258 ms instead of 361 ms (`vectorcraft-cli bench`), with the same pixels.
|
||||
With Overprint Preview or Separations Preview on, the renderer keeps the art it prepared for the preview from one frame to the next. It prepares again the objects that changed, and all of the art when Discard White Overprint changes or, with Separations Preview, when the plates shown, the proof setup, the swatches, the Spot Colors option or the color settings change or a profile is loaded. It draws the other objects from the art, geometry and stroke outlines it kept for them.
|
||||
|
||||
Reference in New Issue
Block a user