M6: the missing-image placeholder's stripes stay clean when scaled, and a .fig gives one note (#1646)

This commit is contained in:
bflatastic committed 2026-10-30 16:40:24 +07:00
1 parent db41f2cd37
commit 1aab3fa001
2 files changed
+20 -7

No files matched your search

+7 -4
View File
@@ -42,15 +42,18 @@ fn a_missing_image_keeps_its_name_and_shows_as_missing() {
}
let note = "image fills whose image isn't in the file show a missing-image placeholder and keep its name";
assert_eq!(warnings.iter().filter(|w| w.starts_with(note)).count(), 1, "{warnings:?}");
assert!(!warnings.iter().any(|w| w.contains("without image data")), "the REST JSON note isn't given too: {warnings:?}");
}
#[test]
fn the_placeholder_reads_as_missing_not_as_grey() {
let png = image::load_from_memory(missing_image_placeholder().unwrap()).unwrap().to_rgba8();
let mut colours: Vec<[u8; 4]> = png.pixels().map(|p| p.0).collect();
colours.sort();
colours.dedup();
assert_eq!(colours.len(), 2, "two tones: stripes over a light ground, not one flat grey");
// Two tones (their anti-aliased edges between): darker stripes over a light ground, not one
// flat grey.
let count = |c: u8| png.pixels().filter(|p| p.0 == [c, c, c, 0xFF]).count();
let all = png.pixels().count();
let (ground, stripe) = (count(0xE6), count(0xB4));
assert!(ground > all / 2 && stripe > all / 8 && ground + stripe > all * 3 / 4, "{ground} ground, {stripe} stripe of {all}");
// A real image of one colour is never taken for the placeholder.
let flat = figcraft_doc::ImageData { bytes: std::sync::Arc::new(vec![1, 2, 3]), width: 64, height: 64 };
assert!(!is_missing_image(&flat));
+13 -3
View File
@@ -1053,6 +1053,8 @@ impl<'a> Ctx<'a> {
/// Cross-references that need every layer imported first.
fn finish(&mut self) {
// A `.fig` import (its sources are taken below) noted each missing image already.
let fig = self.fig_sources.is_some();
self.resolve_swap_defaults();
// Variants share their set's property definitions.
for (set, defs) in std::mem::take(&mut self.set_defs) {
@@ -1086,7 +1088,7 @@ impl<'a> Ctx<'a> {
flows.push(Flow { name, start: id, description: String::new() });
}
}
if self.missing_images > 0 && self.fig_sources.is_none() {
if self.missing_images > 0 && !fig {
let n = self.missing_images;
let (s, verb) = if n == 1 { ("", "shows") } else { ("s", "show") };
self.warn(format!(
@@ -1472,7 +1474,7 @@ fn sane(p: &BezPath) -> bool {
}
/// Side of the placeholder image (large enough that scaling it up stays flat).
const PLACEHOLDER_SIDE: u32 = 64;
const PLACEHOLDER_SIDE: u32 = 256;
/// The PNG standing in for image fills whose bytes aren't available (#1646): light grey crossed
/// by darker diagonal stripes, so it reads as an image that is missing, not as a grey the design
@@ -1480,8 +1482,16 @@ const PLACEHOLDER_SIDE: u32 = 64;
pub fn missing_image_placeholder() -> Option<&'static [u8]> {
static PNG: std::sync::OnceLock<Option<Vec<u8>>> = std::sync::OnceLock::new();
PNG.get_or_init(|| {
// Stripes 6 px wide every 18 px across the diagonal, their edges anti-aliased, so they
// stay clean when a fill scales the picture up.
let (period, width) = (18.0f32, 6.0f32);
let (ground, stripe) = ([0xE6u8, 0xE6, 0xE6], [0xB4u8, 0xB4, 0xB4]);
let img = image::RgbaImage::from_fn(PLACEHOLDER_SIDE, PLACEHOLDER_SIDE, |x, y| {
if (x + y) % 16 < 5 { image::Rgba([0xB8, 0xB8, 0xB8, 0xFF]) } else { image::Rgba([0xE6, 0xE6, 0xE6, 0xFF]) }
let t = ((x + y) as f32 + 1.0) / std::f32::consts::SQRT_2 % period;
let inside = if t < width { t.min(width - t) + 0.5 } else { 0.5 - (t - width).min(period - t) };
let k = inside.clamp(0.0, 1.0);
let mix = |i: usize| (f32::from(ground[i]) + (f32::from(stripe[i]) - f32::from(ground[i])) * k).round() as u8;
image::Rgba([mix(0), mix(1), mix(2), 0xFF])
});
let mut out = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut out), image::ImageFormat::Png).ok()?;