Effects: Glass loads its texture from an image file

Effect › Distort › Glass… offered only its four built-in textures. The
reference app's Texture menu also has Load Texture, which takes an
image file as the glass surface.

The Glass dialog now has a Load Texture… button. It reads a PNG, JPEG,
GIF, WebP, TIFF, BMP, PSD or PSB file within the pixel allowance the
renderer draws images in, and the Texture menu gains Loaded Texture.
The effect stores the image's gray levels in `textureImage` as a
grayscale PNG in base64, so the document saves the texture with the
effect, and copies of the effect carry it. effect.apply and
effect.setParams take `textureImage` as {path}, {name, dataBase64} or
the stored string; any other value is an error.

The texture lies one image pixel per point at Scaling 100%, centered on
the object and repeated. Each pixel's luma is the surface's height,
from 0 for black to 1 pt for white, and a transparent pixel counts as
middle gray. A Glass without `textureImage` draws and saves as before.

Docs: docs/mcp.md (Glass textures), docs/effects-parity.md,
docs/gaps.md, docs/target-app-parity.md.
This commit is contained in:
dotCooCoo
2026-10-16 06:16:35 +07:00
committed by bflatastic
parent 7e9d0be723
commit c586cecf75
33 changed files with 615 additions and 19 deletions
+38
View File
@@ -5,6 +5,7 @@ use std::io::Cursor;
use serde_json::{Value, json};
use vectorcraft_doc::{Document, ImageBlob, ImageObject, Node, NodeKind};
use vectorcraft_geom::Affine;
use vectorcraft_render::effects::pixel::LoadedTexture;
use super::super::*;
use super::pdfimport::LoadOptions;
@@ -491,6 +492,43 @@ fn as_png(img: image::RgbaImage, ppi: Option<(f64, f64)>) -> Result<RasterImage>
stored(png, "image/png", size, ppi)
}
/// Effect › Distort › Glass › Load Texture…: encoded image `bytes`, in any format [`raster_image`]
/// reads, as the `textureImage` Glass stores ([`LoadedTexture::to_stored`]). The pixels are decoded
/// within [`MAX_RASTER_ALLOC`], counted as RGBA, as the renderer decodes them (#1717); a CMYK image
/// is converted with the active color settings.
pub fn texture_image(bytes: &[u8]) -> Result<String> {
if let Some(note) = too_large_note(bytes) {
return Err(err(note));
}
let RasterImage { blob, width, height, .. } = raster_image(bytes)?;
// The allowance counts the pixels as RGBA: a gray image takes four times its file's samples (#1717).
if u64::from(width) * u64::from(height) * 4 > MAX_RASTER_ALLOC {
return Err(err(format!("an image of {width} × {height} pixels is too large to draw")));
}
let texture = match blob.cmyk() {
Some(inks) => {
let cms = vectorcraft_color::cms::active();
let rgba: Vec<u8> = (inks.data.as_chunks::<4>().0.iter())
.flat_map(|c| {
let rgb = cms.cmyk_to_srgb(c.map(|v| f32::from(v) / 255.0), false);
let [r, g, b] = rgb.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8);
[r, g, b, 255]
})
.collect();
LoadedTexture::from_rgba(inks.width, inks.height, &rgba)
}
None => {
let mut reader = image::ImageReader::new(Cursor::new(blob.bytes.as_slice())).with_guessed_format().map_err(err)?;
let mut limits = image::Limits::default();
limits.max_alloc = Some(MAX_RASTER_ALLOC);
reader.limits(limits);
let img = reader.decode().map_err(err)?.to_rgba8();
LoadedTexture::from_rgba(img.width(), img.height(), img.as_raw())
}
};
texture.and_then(|t| t.to_stored()).ok_or_else(|| err("the image can't be read as a texture"))
}
/// An image's encoded `bytes`, `width` × `height` pixels, ready to embed.
fn stored(bytes: Vec<u8>, mime: &'static str, (width, height): (u32, u32), ppi: Option<(f64, f64)>) -> Result<RasterImage> {
if width == 0 || height == 0 {
+1 -1
View File
@@ -60,7 +60,7 @@ pub(crate) use load::native_file;
pub(crate) use load::source;
pub use load::{
Loaded, RasterImage, ReadFile, detect, file_name, load, load_with, open_bytes, open_bytes_with, open_read, open_template, raster_image,
read_for_open,
read_for_open, texture_image,
};
pub use load::{losses_summary, overwrite_losses};
pub(crate) use native::preview_png;