mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-26 18:56:55 -06:00
Render: an image that can't be decoded shows as a crossed box and isn't decoded again
An image whose pixels can't be decoded, such as a PNG cut short by an interrupted download, drew nothing on the canvas and the status bar said nothing. Every render decoded it again, so each frame that drew it paid for a decode that fails. The renderer now notes such an image by its content and does not decode it again. Each render still counts it in decode_failures, so an export reports it as before. An image refused only because the images being drawn leave it no room within twice the decoding allowance is not noted, and the next render tries it again. On the canvas the 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 still draw nothing there. Docs: docs/target-app-parity.md.
This commit is contained in:
@@ -2131,6 +2131,7 @@ impl Renderer {
|
||||
let o = f.opts;
|
||||
(doc.images, self.dim_images.map(f32::to_bits), o.smooth_images, o.image_mips, o.trace_views, o.progressive_placed).hash(&mut h);
|
||||
o.trace_source.as_deref().hash(&mut h);
|
||||
o.mark_undecoded_images.hash(&mut h);
|
||||
}
|
||||
if reads.brushes {
|
||||
self.brushes.library_key(f.doc, self.stamp).hash(&mut h);
|
||||
|
||||
@@ -91,6 +91,11 @@ pub(crate) struct ImageStore {
|
||||
tick: u64,
|
||||
/// [`BlobId`]s per blob byte buffer, by address (the `Weak` keeps it from being reused).
|
||||
ids: HashMap<usize, (Weak<Vec<u8>>, BlobId)>,
|
||||
/// The content keys of images whose pixels can't be decoded ([`Undecoded::Unreadable`]), so
|
||||
/// they aren't decoded again, each with whether [`newly_undecodable`] has counted it.
|
||||
///
|
||||
/// [`Undecoded::Unreadable`]: crate::paint::Undecoded::Unreadable
|
||||
undecodable: HashMap<Arc<str>, bool>,
|
||||
}
|
||||
|
||||
fn pixmap_bytes(pm: &Pixmap) -> usize {
|
||||
@@ -99,7 +104,21 @@ fn pixmap_bytes(pm: &Pixmap) -> usize {
|
||||
|
||||
impl ImageStore {
|
||||
pub(crate) fn new(budget: usize) -> Self {
|
||||
Self { entries: HashMap::new(), bytes: 0, budget, tick: 0, ids: HashMap::new() }
|
||||
Self { entries: HashMap::new(), bytes: 0, budget, tick: 0, ids: HashMap::new(), undecodable: HashMap::new() }
|
||||
}
|
||||
|
||||
/// Whether the image with content key `content` is known not to decode.
|
||||
pub(crate) fn is_undecodable(&self, content: &str) -> bool {
|
||||
self.undecodable.contains_key(content)
|
||||
}
|
||||
|
||||
/// Note that the image with content key `content` can't be decoded. Past 1024 such images the
|
||||
/// notes are dropped and those images are tried again.
|
||||
pub(crate) fn set_undecodable(&mut self, content: Arc<str>) {
|
||||
if self.undecodable.len() >= 1024 {
|
||||
self.undecodable.clear();
|
||||
}
|
||||
self.undecodable.entry(content).or_insert(false);
|
||||
}
|
||||
|
||||
/// The pixels of `key`, marked as just used.
|
||||
@@ -229,6 +248,27 @@ pub(crate) fn remember_size(blob: &ImageBlob, size: (u16, u16)) {
|
||||
store().remember_size(&blob.bytes, size);
|
||||
}
|
||||
|
||||
/// How many of `blobs` a renderer has found it can't decode (they draw nothing, or a crossed box
|
||||
/// with [`RenderOptions::mark_undecoded_images`](crate::RenderOptions::mark_undecoded_images)) that
|
||||
/// no earlier call counted: each such image is counted once in the process.
|
||||
pub fn newly_undecodable<'a>(blobs: impl IntoIterator<Item = &'a ImageBlob>) -> usize {
|
||||
let mut s = store();
|
||||
if s.undecodable.values().all(|counted| *counted) {
|
||||
return 0;
|
||||
}
|
||||
let mut n = 0;
|
||||
for blob in blobs {
|
||||
let Some(id) = s.known_id(&blob.bytes) else { continue };
|
||||
if let Some(counted) = s.undecodable.get_mut(&id.content)
|
||||
&& !*counted
|
||||
{
|
||||
*counted = true;
|
||||
n += 1;
|
||||
}
|
||||
}
|
||||
n
|
||||
}
|
||||
|
||||
/// `pm` at half the size (rounded up), each pixel the mean of the 2×2 it covers (the last row and
|
||||
/// column repeat on odd sizes).
|
||||
pub(crate) fn half(pm: &Pixmap) -> Pixmap {
|
||||
|
||||
@@ -39,7 +39,7 @@ use lru::FrameCache;
|
||||
pub use brush_fx::instance_art;
|
||||
pub use changed::{Changed, changed, view_pixels};
|
||||
pub use effects::stroke::width_outline;
|
||||
pub use images::{IMAGE_CACHE_MAX, image_cache_bytes, set_image_cache_budget};
|
||||
pub use images::{IMAGE_CACHE_MAX, image_cache_bytes, newly_undecodable, set_image_cache_budget};
|
||||
pub use live::expand_live;
|
||||
pub use pattern::render_pattern_swatch;
|
||||
pub use vectorcraft_effects as effects;
|
||||
@@ -186,6 +186,9 @@ pub struct RenderOptions {
|
||||
/// (box-filtered from the full image, kept with the decoded images), so the full image needn't
|
||||
/// stay in memory or be sampled. Off, images always sample their full pixels (exports).
|
||||
pub image_mips: bool,
|
||||
/// Screen views: an image whose pixels can't be decoded draws as a light gray box crossed
|
||||
/// from corner to corner. Off, it draws nothing (exports, which report it instead).
|
||||
pub mark_undecoded_images: bool,
|
||||
/// Screen views while an interaction is in progress (a drag, a dialog's preview): an object
|
||||
/// whose content only moved since an earlier frame draws its cached drop shadows, outer glows
|
||||
/// and filtered content from the rasters made then, at the nearest whole pixel, without
|
||||
@@ -281,6 +284,7 @@ impl Default for RenderOptions {
|
||||
precise: false,
|
||||
supersample: false,
|
||||
image_mips: false,
|
||||
mark_undecoded_images: false,
|
||||
interacting: false,
|
||||
dragging: false,
|
||||
rich_black: false,
|
||||
@@ -2151,6 +2155,8 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if f.opts.mark_undecoded_images && (!outline || f.doc.setup.outline_images) {
|
||||
self.draw_undecoded_mark(ctx, f, im, area);
|
||||
}
|
||||
if outline {
|
||||
let mut p = rect.to_path(0.1);
|
||||
@@ -2159,6 +2165,32 @@ impl Renderer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw the mark of image `im` whose pixels can't be had ([`RenderOptions::mark_undecoded_images`]):
|
||||
/// a light gray box over its frame, crossed from corner to corner with a gray hairline, and
|
||||
/// with `area` (see [`Self::draw_image_in`]) only inside that region.
|
||||
fn draw_undecoded_mark(&mut self, ctx: &mut RenderContext, f: &Frame, im: &vectorcraft_doc::ImageObject, area: Option<&(BezPath, FillRule)>) {
|
||||
let (w, h) = (im.width as f64, im.height as f64);
|
||||
if let Some((bp, rule)) = area {
|
||||
ctx.set_transform(f.view);
|
||||
ctx.set_fill_rule(fill_rule(*rule));
|
||||
ctx.push_clip_layer(bp);
|
||||
}
|
||||
let rect = Rect::new(0.0, 0.0, w, h);
|
||||
ctx.set_transform(f.view * im.xf);
|
||||
ctx.set_paint(f.ink.fixed([128, 128, 128, 40]));
|
||||
ctx.fill_rect(&rect);
|
||||
let mut cross = rect.to_path(0.1);
|
||||
cross.move_to((0.0, 0.0));
|
||||
cross.line_to((w, h));
|
||||
cross.move_to((w, 0.0));
|
||||
cross.line_to((0.0, h));
|
||||
cross.apply_affine(im.xf);
|
||||
self.hairline(ctx, f, &cross, [128, 128, 128, 255]);
|
||||
if area.is_some() {
|
||||
ctx.pop_layer();
|
||||
}
|
||||
}
|
||||
|
||||
/// Name image `key` recoloured by `map` (its hash `hash`) as `derived` (the key the adjusted
|
||||
/// art's image takes): drawing `derived` draws the recoloured pixels, made when first drawn and
|
||||
/// kept with the decoded images. Names past a limit drop the art made with them too, so it is
|
||||
@@ -2276,16 +2308,30 @@ fn blob_key(blob: &vectorcraft_doc::ImageBlob) -> (images::ImageKey, Option<Arc<
|
||||
}
|
||||
|
||||
/// The decoded pixels of `blob` (a CMYK image's inks shown through the working CMYK profile, again
|
||||
/// when the colour settings change), from the shared store, and their key there.
|
||||
/// when the colour settings change), from the shared store, and their key there. An image whose
|
||||
/// pixels can't be decoded is noted in the store and not decoded again; each request for it counts
|
||||
/// a failure in [`decode_failures`].
|
||||
fn blob_pixmap(blob: &vectorcraft_doc::ImageBlob, touch: bool) -> Option<(images::ImageKey, Arc<Pixmap>)> {
|
||||
let (key, lut, size) = blob_key(blob);
|
||||
let pm = match lookup(&key, touch) {
|
||||
Some(pm) => pm,
|
||||
None => {
|
||||
if images::store().is_undecodable(&key.content) {
|
||||
paint::add_decode_failures(1);
|
||||
return None;
|
||||
}
|
||||
// Decoded outside the lock: other renderers carry on meanwhile.
|
||||
let pm = match lut.and_then(|lut| ink::cmyk_pixmap(blob, &lut)) {
|
||||
Some(pm) => pm,
|
||||
None => paint::decode_pixmap(&blob.bytes)?,
|
||||
None => match paint::decode_pixmap_or_why(&blob.bytes) {
|
||||
Ok(pm) => pm,
|
||||
Err(why) => {
|
||||
if why == paint::Undecoded::Unreadable {
|
||||
images::store().set_undecodable(key.content.clone());
|
||||
}
|
||||
return None;
|
||||
}
|
||||
},
|
||||
};
|
||||
images::store().insert(key.clone(), Arc::new(pm))
|
||||
}
|
||||
|
||||
+79
-15
@@ -73,6 +73,23 @@ thread_local! {
|
||||
static DECODE_FAILURES: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
thread_local! {
|
||||
/// The images [`decode`] has tried to decode on this thread.
|
||||
pub(crate) static DECODE_ATTEMPTS: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// 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.
|
||||
Unreadable,
|
||||
/// The images being painted leave no room for it within twice [`MAX_DECODE_ALLOC`]: it may
|
||||
/// be decoded once they are done.
|
||||
NoRoom,
|
||||
}
|
||||
|
||||
/// How many images have failed to decode on this thread so far: an export compares the count
|
||||
/// before and after it draws to say it left images out. Images that other threads (another export,
|
||||
/// the canvas, thumbnails) fail to decode meanwhile are not counted.
|
||||
@@ -80,20 +97,33 @@ pub fn decode_failures() -> u64 {
|
||||
DECODE_FAILURES.with(std::cell::Cell::get)
|
||||
}
|
||||
|
||||
/// `bytes` decoded within [`MAX_DECODE_ALLOC`] ([`decode_within_allowance`]); a failure is counted
|
||||
/// in [`decode_failures`].
|
||||
/// `bytes` decoded within [`MAX_DECODE_ALLOC`] ([`decode_in_room`]); a failure is counted in
|
||||
/// [`decode_failures`].
|
||||
fn decode(bytes: &[u8]) -> Option<image::DynamicImage> {
|
||||
let img = decode_within_allowance(bytes).ok();
|
||||
if img.is_none() {
|
||||
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);
|
||||
}
|
||||
img
|
||||
why
|
||||
})
|
||||
}
|
||||
|
||||
/// [`decode_in_room`]'s image or error.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn decode_within_allowance(bytes: &[u8]) -> image::ImageResult<image::DynamicImage> {
|
||||
decode_in_room(bytes).map_err(|(e, _)| e)
|
||||
}
|
||||
|
||||
/// `bytes` decoded within [`MAX_DECODE_ALLOC`], counted as RGBA: the image's size is read from its
|
||||
/// header first, and an image of more than that as RGBA gets a limit error before anything is
|
||||
/// decoded (#1717).
|
||||
pub(crate) fn decode_within_allowance(bytes: &[u8]) -> image::ImageResult<image::DynamicImage> {
|
||||
/// 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 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();
|
||||
@@ -101,15 +131,27 @@ pub(crate) fn decode_within_allowance(bytes: &[u8]) -> image::ImageResult<image:
|
||||
r.limits(limits);
|
||||
Ok(r)
|
||||
};
|
||||
let (w, h) = reader()?.into_dimensions()?;
|
||||
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 bytes > MAX_DECODE_ALLOC || bytes.saturating_add(pinned) > 2 * MAX_DECODE_ALLOC {
|
||||
return Err(image::ImageError::Limits(image::error::LimitError::from_kind(image::error::LimitErrorKind::InsufficientMemory)));
|
||||
if let Err(why) = room_for(bytes, pinned) {
|
||||
return Err((image::ImageError::Limits(image::error::LimitError::from_kind(image::error::LimitErrorKind::InsufficientMemory)), why));
|
||||
}
|
||||
reader().and_then(|r| r.decode()).map_err(unreadable)
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn room_for(rgba: u64, pinned: u64) -> Result<(), Undecoded> {
|
||||
if rgba > MAX_DECODE_ALLOC {
|
||||
Err(Undecoded::Unreadable)
|
||||
} else if rgba.saturating_add(pinned) > 2 * MAX_DECODE_ALLOC {
|
||||
Err(Undecoded::NoRoom)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
reader()?.decode()
|
||||
}
|
||||
|
||||
/// `img` shrunk, keeping its proportions, to fit a pixmap's sides (at most `u16::MAX` pixels).
|
||||
@@ -124,12 +166,19 @@ fn fit_pixmap(img: image::DynamicImage) -> image::DynamicImage {
|
||||
img.resize_exact(nw, nh, image::imageops::FilterType::Triangle)
|
||||
}
|
||||
|
||||
/// Decode encoded image bytes into a premultiplied pixmap (shrunk to fit a pixmap's sides).
|
||||
/// [`decode_pixmap_or_why`]'s pixels.
|
||||
#[cfg(test)]
|
||||
pub fn decode_pixmap(bytes: &[u8]) -> Option<vello_cpu::Pixmap> {
|
||||
pixmap_of(fit_pixmap(decode(bytes)?).to_rgba8())
|
||||
decode_pixmap_or_why(bytes).ok()
|
||||
}
|
||||
|
||||
/// [`decode_pixmap`] shrunk to `w`×`h` pixels when the image is larger (never enlarged).
|
||||
/// Decode encoded image bytes into a premultiplied pixmap (shrunk to fit a pixmap's sides), or say
|
||||
/// why there are no pixels.
|
||||
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)
|
||||
}
|
||||
|
||||
/// [`decode_pixmap_or_why`] shrunk to `w`×`h` pixels when the image is larger (never enlarged).
|
||||
pub fn decode_pixmap_fit(bytes: &[u8], w: u16, h: u16) -> Option<vello_cpu::Pixmap> {
|
||||
let img = decode(bytes)?;
|
||||
let img = if img.width() > w as u32 || img.height() > h as u32 {
|
||||
@@ -163,3 +212,18 @@ fn pixmap_of(img: image::RgbaImage) -> Option<vello_cpu::Pixmap> {
|
||||
.collect();
|
||||
Some(vello_cpu::Pixmap::from_parts(data, w as u16, h as u16))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// An image over the allowance as RGBA never fits, so it is noted as undecodable; one that only
|
||||
/// the images being painted leave no room for is not, and is tried again later.
|
||||
#[test]
|
||||
fn only_the_room_painted_images_take_is_tried_again() {
|
||||
assert_eq!(room_for(MAX_DECODE_ALLOC + 1, 0), Err(Undecoded::Unreadable));
|
||||
assert_eq!(room_for(MAX_DECODE_ALLOC + 1, 2 * MAX_DECODE_ALLOC), Err(Undecoded::Unreadable));
|
||||
assert_eq!(room_for(MAX_DECODE_ALLOC, MAX_DECODE_ALLOC + 1), Err(Undecoded::NoRoom));
|
||||
assert_eq!(room_for(MAX_DECODE_ALLOC, MAX_DECODE_ALLOC), Ok(()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -946,6 +946,87 @@ fn an_image_too_large_as_rgba_is_not_decoded() {
|
||||
assert!(matches!(huge, Err(image::ImageError::Limits(_))), "{huge:?}");
|
||||
}
|
||||
|
||||
/// A PNG of `w` × `h` noisy pixels cut off halfway through its bytes: its header reads, and its
|
||||
/// pixels can't be decoded.
|
||||
fn truncated_png(w: u32, h: u32) -> Vec<u8> {
|
||||
let img = image::RgbaImage::from_fn(w, h, |x, y| {
|
||||
let v = (x * 7919 + y * 104_729).wrapping_mul(2_654_435_761).to_le_bytes();
|
||||
image::Rgba([v[0], v[1], v[2], 255])
|
||||
});
|
||||
let mut png = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
png.truncate(png.len() / 2);
|
||||
png
|
||||
}
|
||||
|
||||
/// An image whose pixels can't be decoded is tried once: later renders find the store's note of it,
|
||||
/// and each render counts it in `decode_failures` for an export to report. On screen
|
||||
/// (`mark_undecoded_images`) it draws as a crossed gray box; an export draws nothing there.
|
||||
#[test]
|
||||
fn an_undecodable_image_is_tried_once_and_marked_on_screen() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
|
||||
let png = truncated_png(61, 47);
|
||||
assert_eq!(image::ImageReader::new(std::io::Cursor::new(&png)).with_guessed_format().unwrap().into_dimensions().unwrap(), (61, 47));
|
||||
let mut d = Document::new(80.0, 60.0);
|
||||
d.images.insert("cut".into(), ImageBlob::new("image/png", png));
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject {
|
||||
key: "cut".into(),
|
||||
width: 61,
|
||||
height: 47,
|
||||
xf: Affine::translate((10.0, 5.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
d.insert(Some(d.layers[0].id), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
// The alpha summed over the image's frame.
|
||||
let alpha = |r: &Rendered| (10..71).flat_map(|x| (5..52).map(move |y| u64::from(r.pixel(x, y)[3]))).sum::<u64>();
|
||||
let attempts = || paint::DECODE_ATTEMPTS.with(std::cell::Cell::get);
|
||||
let (tried, failed) = (attempts(), decode_failures());
|
||||
let screen = RenderOptions { mark_undecoded_images: true, ..Default::default() };
|
||||
let mut r = Renderer::new();
|
||||
for render in 1..=2 {
|
||||
let shown = r.render(&d, 80, 60, Affine::IDENTITY, &screen);
|
||||
assert!(alpha(&shown) > 0, "render {render}: the crossed box shows");
|
||||
assert_eq!(decode_failures() - failed, render, "render {render}: counted");
|
||||
}
|
||||
assert_eq!(attempts() - tried, 1, "tried once");
|
||||
let export = Renderer::new().render(&d, 80, 60, Affine::IDENTITY, &RenderOptions::default());
|
||||
assert_eq!(alpha(&export), 0, "an export draws nothing there");
|
||||
assert_eq!((attempts() - tried, decode_failures() - failed), (1, 3), "and counts it without decoding it again");
|
||||
}
|
||||
|
||||
/// The rasters a renderer keeps of an object's raster effects are made again when
|
||||
/// `mark_undecoded_images` changes: a blurred image whose pixels can't be decoded shows its blurred
|
||||
/// mark only while the option is on.
|
||||
#[test]
|
||||
fn raster_effects_follow_the_undecoded_image_mark() {
|
||||
use vectorcraft_doc::{Effect, ImageBlob, ImageObject, NodeKind};
|
||||
let mut d = Document::new(80.0, 60.0);
|
||||
d.images.insert("cut".into(), ImageBlob::new("image/png", truncated_png(43, 31)));
|
||||
let id = d.alloc_id();
|
||||
let im = ImageObject {
|
||||
key: "cut".into(),
|
||||
width: 43,
|
||||
height: 31,
|
||||
xf: Affine::translate((15.0, 12.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let mut n = Node::new(id, NodeKind::Image(Box::new(im)));
|
||||
n.appearance.effects.push(Effect { id: "blur.gaussian".into(), params: serde_json::json!({"radius": 2.0}), visible: true });
|
||||
d.insert(Some(d.layers[0].id), 0, n).unwrap();
|
||||
let alpha = |r: &Rendered| r.pixels.chunks(4).map(|p| u64::from(p[3])).sum::<u64>();
|
||||
let mut r = Renderer::new();
|
||||
for mark in [true, false, true] {
|
||||
let opts = RenderOptions { mark_undecoded_images: mark, ..Default::default() };
|
||||
let a = alpha(&r.render(&d, 80, 60, Affine::IDENTITY, &opts));
|
||||
assert_eq!(a > 0, mark, "mark_undecoded_images {mark}: alpha {a}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A gray image within the allowance draws its gray levels.
|
||||
#[test]
|
||||
fn a_gray_image_draws_its_gray_levels() {
|
||||
|
||||
@@ -521,6 +521,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
isolated: key.isolated.map(vectorcraft_doc::NodeId),
|
||||
// Images far smaller on screen than they are sample their mip levels (#1244).
|
||||
image_mips: true,
|
||||
// An image whose pixels can't be decoded shows as a crossed box, and the status message
|
||||
// below reports it.
|
||||
mark_undecoded_images: true,
|
||||
// During a drag or a dialog's preview, objects that only moved draw their raster
|
||||
// effects from the cached rasters of an earlier frame (#1257).
|
||||
interacting: key.interacting,
|
||||
@@ -571,6 +574,12 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
}
|
||||
// The status bar reports each image that a renderer (this thread's or the worker's) could not
|
||||
// decode, once per image in the process.
|
||||
let undecoded = vectorcraft_render::newly_undecodable(doc.images.values());
|
||||
if undecoded > 0 {
|
||||
app.status(format!("{undecoded} image(s) couldn't be decoded and show as crossed boxes"));
|
||||
}
|
||||
if let (Some(tex), Some(k)) = (&app.canvas.texture, &app.canvas.key)
|
||||
&& k.doc == key.doc
|
||||
{
|
||||
@@ -5704,6 +5713,55 @@ mod tests {
|
||||
assert_eq!(pink(&mut app), Some(on), "on again, as before");
|
||||
}
|
||||
|
||||
/// Images whose pixels can't be decoded draw as crossed boxes on the canvas, and the status
|
||||
/// bar reports each of them once: two such images give one message, and later frames that
|
||||
/// draw them again give none.
|
||||
#[test]
|
||||
fn undecodable_images_are_marked_and_reported_once() {
|
||||
use vectorcraft_doc::{ImageBlob, ImageObject, NodeKind};
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap();
|
||||
app.canvas.worker_started = true; // render on this thread
|
||||
// PNGs of noisy pixels cut off halfway: their headers read, their pixels don't decode.
|
||||
let cut = |w: u32| {
|
||||
let img = image::RgbaImage::from_fn(w, 41, |x, y| image::Rgba([(x * 37 + y * 101) as u8, (x * y) as u8, (x ^ y) as u8, 255]));
|
||||
let mut png = vec![];
|
||||
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png).unwrap();
|
||||
png.truncate(png.len() / 2);
|
||||
png
|
||||
};
|
||||
{
|
||||
let st = app.session.doc_mut().unwrap();
|
||||
let d = std::sync::Arc::make_mut(&mut st.doc);
|
||||
for (i, w) in [53u32, 59].into_iter().enumerate() {
|
||||
let key = format!("cut{i}");
|
||||
d.images.insert(key.clone(), ImageBlob::new("image/png", cut(w)));
|
||||
let im = ImageObject {
|
||||
key,
|
||||
width: w,
|
||||
height: 41,
|
||||
xf: Affine::translate((40.0 + 150.0 * i as f64, 100.0)),
|
||||
link: None,
|
||||
placement: Default::default(),
|
||||
pixels: None,
|
||||
};
|
||||
let (id, layer) = (d.alloc_id(), d.layers[0].id);
|
||||
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
||||
}
|
||||
st.revision += 1;
|
||||
}
|
||||
let ctx = egui::Context::default();
|
||||
app.status("");
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
assert!(app.canvas.shown.as_ref().is_some_and(|(_, o)| o.mark_undecoded_images), "the canvas marks them");
|
||||
assert_eq!(app.ui.status, "2 image(s) couldn't be decoded and show as crossed boxes");
|
||||
app.status("");
|
||||
app.session.doc_mut().unwrap().revision += 1;
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
frame(&mut app, &ctx, vec![]);
|
||||
assert_eq!(app.ui.status, "", "said once");
|
||||
}
|
||||
|
||||
/// A drag of an object with raster effects (#1257): the drag's frames render while
|
||||
/// interacting and draw the object's drop shadow and filtered content from the rasters of the
|
||||
/// frame before; the frame after the drag renders without `interacting`, on the render worker,
|
||||
|
||||
@@ -4274,6 +4274,7 @@ axis Both И двете
|
||||
@msg {_1} is already used by {_2} {_1} вече се използва от {_2}
|
||||
@msg {_1} is reserved by the system {_1} е запазено от системата
|
||||
@msg {_1} linked file(s) can't be read here: their previews show свързаните файлове {_1} не могат да бъдат прочетени тук: показват се техните прегледи
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes изображенията ({undecoded}) не могат да бъдат декодирани: показват се като зачеркнати рамки
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions свързаните изображения {_1} са променени на диска: „Обновяване на връзките“ показва новите версии
|
||||
@msg Updated {_1} linked file(s) changed on disk Обновени са свързани файлове {_1}, променени на диска
|
||||
@msg {_1}: not saved, so not closed {_1}: не е запазен, затова не е затворен
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4682,6 +4682,7 @@ imageTrace Outline Obrys
|
||||
@msg {_1} is already used by {_2} {_1} už používá {_2}
|
||||
@msg {_1} is reserved by the system Zkratku {_1} si vyhrazuje systém
|
||||
@msg {_1} linked file(s) can't be read here: their previews show propojené soubory ({_1}) zde nelze přečíst: zobrazují se jejich náhledy
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes obrázky ({undecoded}) nelze dekódovat: zobrazují se jako přeškrtnuté rámečky
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions propojené obrázky ({_1}) se na disku změnily: nové verze zobrazí Aktualizovat propojení
|
||||
@msg {_1}: not saved, so not closed {_1}: neuloženo, proto nezavřeno
|
||||
@msg {bpp}-bit bitmaps can't be read {bpp}bitové bitmapy nelze přečíst
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4305,6 +4305,7 @@ theme Appearance Erscheinungsbild
|
||||
@msg {_1} is already used by {_2} {_1} wird bereits von {_2} verwendet
|
||||
@msg {_1} is reserved by the system {_1} ist vom System reserviert
|
||||
@msg {_1} linked file(s) can't be read here: their previews show {_1} verknüpfte Datei(en) können hier nicht gelesen werden: Ihre Vorschauen werden angezeigt
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes {undecoded} Bild(er) konnten nicht dekodiert werden und werden als durchgekreuzte Rahmen angezeigt
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} verknüpfte(s) Bild(er) wurde(n) auf dem Datenträger geändert: „Verknüpfungen aktualisieren“ zeigt die neuen Versionen
|
||||
@msg {_1}: not saved, so not closed {_1}: nicht gespeichert, daher nicht geschlossen
|
||||
@msg {bpp}-bit bitmaps can't be read {bpp}-Bit-Bitmaps können nicht gelesen werden
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4338,6 +4338,7 @@ theme Appearance Εμφάνιση
|
||||
@msg {_1} is already used by {_2} το {_1} χρησιμοποιείται ήδη από το {_2}
|
||||
@msg {_1} is reserved by the system Το {_1} είναι δεσμευμένο από το σύστημα
|
||||
@msg {_1} linked file(s) can't be read here: their previews show {_1} συνδεδεμένα αρχεία δεν μπορούν να διαβαστούν εδώ: εμφανίζονται οι προεπισκοπήσεις τους
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes {undecoded} εικόνες δεν ήταν δυνατό να αποκωδικοποιηθούν: εμφανίζονται ως πλαίσια με σταυρό
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} συνδεδεμένες εικόνες άλλαξαν στον δίσκο: η Ενημέρωση συνδέσμων εμφανίζει τις νέες εκδόσεις
|
||||
@msg Updated {_1} linked file(s) changed on disk Ενημερώθηκαν {_1} συνδεδεμένα αρχεία που άλλαξαν στον δίσκο
|
||||
@msg {_1}: not saved, so not closed {_1}: δεν αποθηκεύτηκε και επομένως δεν έκλεισε
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2069 and column 19.
|
@@ -4380,6 +4380,7 @@ axis Both Ambos
|
||||
@msg {_1} is already used by {_2} {_1} ya lo usa {_2}
|
||||
@msg {_1} is reserved by the system {_1} está reservado por el sistema
|
||||
@msg {_1} linked file(s) can't be read here: their previews show aquí no se pueden leer {_1} archivo(s) vinculado(s): se muestran sus previsualizaciones
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes no se pudieron descodificar {undecoded} imagen(es): se muestran como recuadros tachados
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} imagen(es) vinculada(s) cambiaron en el disco: Actualizar vínculos muestra las versiones nuevas
|
||||
@msg Updated {_1} linked file(s) changed on disk Se actualizaron {_1} archivo(s) vinculado(s) que cambiaron en el disco
|
||||
@msg {_1}: not saved, so not closed {_1}: no se guardó, así que no se cerró
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4589,6 +4589,7 @@ imageTrace Outline Ääriviiva
|
||||
@msg {_1} is already used by {_2} {_1} on jo käytössä kohteella {_2}
|
||||
@msg {_1} is reserved by the system {_1} on järjestelmän varaama
|
||||
@msg {_1} linked file(s) can't be read here: their previews show {_1} linkitettyä tiedostoa ei voi lukea täällä: niiden esikatselut näytetään
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes {undecoded} kuvaa ei voitu purkaa: ne näkyvät ristillä merkittyinä ruutuina
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} linkitettyä kuvaa muuttui levyllä: Päivitä linkit näyttää uudet versiot
|
||||
@msg {_1}: not saved, so not closed {_1}: ei tallennettu, joten ei suljettu
|
||||
@msg {bpp}-bit bitmaps can't be read {bpp}-bittisiä bittikarttoja ei voi lukea
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4377,6 +4377,7 @@ axis Both Les deux
|
||||
@msg {_1} is already used by {_2} {_1} est déjà utilisé par {_2}
|
||||
@msg {_1} is reserved by the system {_1} est réservé par le système
|
||||
@msg {_1} linked file(s) can't be read here: their previews show {_1} fichier(s) lié(s) illisible(s) ici : leurs aperçus sont affichés
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes {undecoded} image(s) impossible(s) à décoder, affichée(s) sous forme de cadres barrés d’une croix
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} image(s) liée(s) modifiée(s) sur le disque : Mettre à jour les liens affiche les nouvelles versions
|
||||
@msg Updated {_1} linked file(s) changed on disk {_1} fichier(s) lié(s) modifié(s) sur le disque mis à jour
|
||||
@msg {_1}: not saved, so not closed {_1} : non enregistré, donc non fermé
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -5141,6 +5141,7 @@ imageTrace Outline Körvonal
|
||||
@msg {_1} is already used by {_2} már foglalt: {_1} (használja: {_2})
|
||||
@msg {_1} is reserved by the system A(z) {_1} a rendszer számára foglalt
|
||||
@msg {_1} linked file(s) can't be read here: their previews show Itt nem olvasható csatolt fájlok: {_1}; az előnézetük látszik
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes Nem dekódolható képek: {undecoded}; áthúzott keretként látszanak
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions A lemezen módosult csatolt képek: {_1}; a Csatolások frissítése az új változatokat mutatja
|
||||
@msg {_1}: not saved, so not closed {_1}: nincs mentve, ezért nem záródott be
|
||||
@msg {bpp}-bit bitmaps can't be read ilyen bitmélységű bitkép nem olvasható: {bpp}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4420,6 +4420,7 @@ axis Both Entrambi
|
||||
@msg {_1} is already used by {_2} {_1} è già usato da {_2}
|
||||
@msg {_1} is reserved by the system {_1} è riservato dal sistema
|
||||
@msg {_1} linked file(s) can't be read here: their previews show file collegati non leggibili qui: {_1}; vengono mostrate le loro anteprime
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes immagini non decodificabili: {undecoded}; vengono mostrate come riquadri barrati da una croce
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions immagini collegate modificate su disco: {_1}; Aggiorna collegamenti mostra le nuove versioni
|
||||
@msg Updated {_1} linked file(s) changed on disk File collegati modificati su disco aggiornati: {_1}
|
||||
@msg {_1}: not saved, so not closed {_1}: non salvato, quindi non chiuso
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4382,6 +4382,7 @@ swatch Bright Violet ビビッドバイオレット
|
||||
@msg no such layer or object 指定されたレイヤーまたはオブジェクトが見つかりません
|
||||
@msg `{tool}` isn't a Liquify tool `{tool}` はゆがみツールではありません
|
||||
@msg {_1} linked file(s) can't be read here: their previews show この環境では {_1} 個のリンクファイルを読み込めません: 代わりにプレビューを表示しています
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes {undecoded} 個の画像をデコードできませんでした: バツ印の付いた枠で表示しています
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} 個のリンク画像がディスク上で変更されました: 「リンクを更新」で新しいバージョンを表示できます
|
||||
@msg Updated {_1} linked file(s) changed on disk ディスク上で変更された {_1} 個のリンクファイルを更新しました
|
||||
@msg pick a colour, gradient or pattern to save as a swatch スウォッチとして保存するカラー、グラデーション、またはパターンを選択してください
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4136,6 +4136,7 @@ font-technology Variable ცვლადი
|
||||
@msg {_1} is already used by {_2} {_1} უკვე გამოიყენება აქ: {_2}
|
||||
@msg {_1} is reserved by the system {_1} დაჯავშნილია სისტემის მიერ
|
||||
@msg {_1} linked file(s) can't be read here: their previews show მიბმული ფაილების წაკითხვა აქ ვერ ხერხდება ({_1}): ჩანს მათი გადახედვა
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes სურათების დეკოდირება ვერ მოხერხდა ({undecoded}): ისინი ჯვრით გადახაზული ჩარჩოებით ჩანს
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions მიბმული გამოსახულებები შეიცვალა დისკზე ({_1}): ბმულების განახლება აჩვენებს ახალ ვერსიებს
|
||||
@msg {_1}: not saved, so not closed {_1}: არ შენახულა, ამიტომ არ დაიხურა
|
||||
@msg {bpp}-bit bitmaps can't be read {bpp}-ბიტიანი bitmap-ების წაკითხვა ვერ ხერხდება
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4314,6 +4314,7 @@ theme Appearance 외관
|
||||
@msg no such layer or object 해당 레이어나 개체가 없습니다
|
||||
@msg `{tool}` isn't a Liquify tool `{tool}`은(는) 픽셀 유동화 도구가 아닙니다
|
||||
@msg {_1} linked file(s) can't be read here: their previews show 링크된 파일 {_1}개를 여기서 읽을 수 없습니다: 미리 보기가 표시됩니다
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes 이미지 {undecoded}개를 디코딩할 수 없습니다: X 표시가 된 상자로 표시됩니다
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions 링크된 이미지 {_1}개가 디스크에서 바뀌었습니다: 링크 업데이트를 하면 새 버전이 표시됩니다
|
||||
@msg Updated {_1} linked file(s) changed on disk 디스크에서 바뀐 링크된 파일 {_1}개를 업데이트했습니다
|
||||
@msg pick a colour, gradient or pattern to save as a swatch 견본으로 저장할 색상, 그레이디언트 또는 패턴을 선택하세요
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4507,6 +4507,7 @@ imageTrace Outline Kontur
|
||||
@msg {_1} is already used by {_2} {_1} jest już używany przez {_2}
|
||||
@msg {_1} is reserved by the system Skrót {_1} jest zarezerwowany przez system
|
||||
@msg {_1} linked file(s) can't be read here: their previews show połączonych plików nie można tu odczytać ({_1}): wyświetlane są ich podglądy
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes nie udało się zdekodować obrazów ({undecoded}): są wyświetlane jako przekreślone ramki
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions połączone obrazy zmienione na dysku: {_1} — Aktualizuj łącza pokaże nowe wersje
|
||||
@msg {_1}: not saved, so not closed {_1}: nie zapisano, więc nie zamknięto
|
||||
@msg {bpp}-bit bitmaps can't be read nie można odczytać bitmap {bpp}-bitowych
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -3953,6 +3953,7 @@ axis Both Ambos
|
||||
@msg {_1} is already used by {_2} {_1} já é usado por {_2}
|
||||
@msg {_1} is reserved by the system {_1} é reservado pelo sistema
|
||||
@msg {_1} linked file(s) can't be read here: their previews show {_1} arquivo(s) vinculado(s) não podem ser lidos aqui: suas visualizações são exibidas
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes não foi possível decodificar {undecoded} imagem(ns): elas aparecem como caixas marcadas com um X
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} imagem(ns) vinculada(s) mudou(aram) no disco: Atualizar links mostra as novas versões
|
||||
@msg {_1}: not saved, so not closed {_1}: não salvo, portanto não fechado
|
||||
@msg {bpp}-bit bitmaps can't be read bitmaps de {bpp} bits não podem ser lidos
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -4416,6 +4416,7 @@ axis Both Оба
|
||||
@msg {_1} is already used by {_2} {_1} уже используется {_2}
|
||||
@msg {_1} is reserved by the system Сочетание {_1} зарезервировано системой
|
||||
@msg {_1} linked file(s) can't be read here: their previews show здесь невозможно прочитать связанных файлов: {_1}: отображаются их предварительные просмотры
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes не удалось декодировать изображения ({undecoded}): они показаны как перечёркнутые рамки
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions связанных изображений ({_1}) изменились на диске: «Обновить связи» покажет новые версии
|
||||
@msg Updated {_1} linked file(s) changed on disk Обновлено связанных файлов, измененных на диске: {_1}
|
||||
@msg {_1}: not saved, so not closed {_1}: не сохранён, поэтому не закрыт
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -4289,6 +4289,7 @@ axis Both Båda
|
||||
@msg {_1} is already used by {_2} {_1} används redan av {_2}
|
||||
@msg {_1} is reserved by the system {_1} är reserverat av systemet
|
||||
@msg {_1} linked file(s) can't be read here: their previews show {_1} länkade fil(er) kan inte läsas här: deras förhandsgranskningar visas
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes {undecoded} bild(er) kunde inte avkodas och visas som överkryssade rutor
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} länkade bild(er) har ändrats på disken: Uppdatera länkar visar de nya versionerna
|
||||
@msg Updated {_1} linked file(s) changed on disk Uppdaterade {_1} länkade fil(er) som ändrats på disken
|
||||
@msg {_1}: not saved, so not closed {_1}: inte sparat, så inte stängt
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4135,6 +4135,7 @@ axis Both Her İkisi
|
||||
@msg {_1} is a live effect (Effect › Plug-ins) {_1} canlı bir efekttir (Efekt › Eklentiler)
|
||||
@msg {_1} is already used by {_2} {_1} zaten {_2} tarafından kullanılıyor
|
||||
@msg {_1} linked file(s) can't be read here: their previews show {_1} bağlantılı dosya(lar) burada okunamıyor: önizlemeleri gösteriliyor
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes {undecoded} görüntünün kodu çözülemedi: çarpı işaretli kutular olarak gösteriliyor
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} bağlantılı görseller diskte değiştirildi: Güncelleme Bağlantıları yeni sürümleri gösteriyor
|
||||
@msg Updated {_1} linked file(s) changed on disk Güncellenmiş {_1} bağlantılı dosya(lar) diskte değiştirildi
|
||||
@msg {_1}: not saved, so not closed {_1}: kaydedilmedi, dolayısıyla kapatılmadı
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -4404,6 +4404,7 @@ axis Both Обидві
|
||||
@msg {_1} is already used by {_2} {_1} вже використовується {_2}
|
||||
@msg {_1} is reserved by the system Сполучення {_1} зарезервоване системою
|
||||
@msg {_1} linked file(s) can't be read here: their previews show тут неможливо прочитати пов’язані файли: {_1}: відображаються їх попередні перегляди
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes не вдалося декодувати зображення ({undecoded}): вони показані як перекреслені рамки
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions пов’язаних зображень ({_1}) змінилися на диску: «Оновити зв’язки» покаже нові версії
|
||||
@msg Updated {_1} linked file(s) changed on disk Оновлено пов’язаних файлів, змінених на диску: {_1}
|
||||
@msg {_1}: not saved, so not closed {_1}: не збережено, тому не закрито
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 748 and column 40.
|
@@ -4799,6 +4799,7 @@ theme Appearance Giao diện
|
||||
@msg {_1} is already used by {_2} {_1} đã được dùng bởi {_2}
|
||||
@msg {_1} is reserved by the system {_1} được hệ thống dành riêng
|
||||
@msg {_1} linked file(s) can't be read here: their previews show {_1} tệp liên kết không thể đọc ở đây: bản xem trước được hiển thị
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes {undecoded} hình ảnh không thể giải mã: chúng hiển thị dưới dạng khung có dấu gạch chéo
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions {_1} ảnh liên kết đã thay đổi trên đĩa: Cập nhật liên kết hiển thị phiên bản mới
|
||||
@msg {_1}: not saved, so not closed {_1}: chưa lưu, nên không đóng
|
||||
@msg {bpp}-bit bitmaps can't be read bitmap {bpp}-bit không thể đọc được
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 4068 and column 10.
|
@@ -4111,6 +4111,7 @@ imageTrace Outline 轮廓
|
||||
@msg no such layer or object 没有此图层或对象
|
||||
@msg `{tool}` isn't a Liquify tool `{tool}` 不是液化工具
|
||||
@msg {_1} linked file(s) can't be read here: their previews show 此处无法读取 {_1} 个链接文件:显示的是其预览
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes 无法解码 {undecoded} 张图像:它们显示为打叉的方框
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions 有 {_1} 个链接图像已在磁盘上更改:“更新链接”可显示新版本
|
||||
@msg pick a colour, gradient or pattern to save as a swatch 请选取要保存为色板的颜色、渐变或图案
|
||||
@msg Packaged {n} file(s) in {folder} {note} 已将 {n} 个文件打包到 {folder}{note}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -2074,6 +2074,7 @@ file-filter Templates 範本
|
||||
@msg no such layer or object 沒有這個圖層或物件
|
||||
@msg `{tool}` isn't a Liquify tool `{tool}` 不是液化工具
|
||||
@msg {_1} linked file(s) can't be read here: their previews show 此處無法讀取 {_1} 個連結檔案:顯示的是其預覽
|
||||
@msg {undecoded} image(s) couldn't be decoded and show as crossed boxes 無法解碼 {undecoded} 張影像:它們顯示為打叉的方框
|
||||
@msg {_1} linked image(s) changed on disk: Update Links shows the new versions 有 {_1} 個連結影像已在磁碟上變更:「更新連結」可顯示新版本
|
||||
@msg pick a colour, gradient or pattern to save as a swatch 請挑選要儲存為色票的顏色、漸層或圖樣
|
||||
@msg Packaged {n} file(s) in {folder} {note} 已將 {n} 個檔案封裝到 {folder}{note}
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -985,6 +985,7 @@ 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.
|
||||
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