craft-mesh: retain glTF texture and sampler inventory (#1151)

* craft-mesh: expose unreferenced glTF texture variants

* mesh: validate texture inventory image indices

* Fix PR 1151 validation and workspace checks

* Fix remaining Rust 1.95 workspace lint findings

* Fix proof workspace Clippy warning

* Fix remaining workspace Clippy findings

* Fix video all-features Clippy warning

* KRA: parse nested layers without recursion

* Preserve KRA layer limit traversal order

* Avoid expect in iterative KRA parser

* OpenRaster: parse nested stacks without recursion

* Plate tests honor optional font fixtures

* Keep bundled font index bytes unchanged

* Preserve font build license fixture bytes

* ci: allow longer workspace test matrix

* test: respect Windows Hunspell path behavior

* ci: provision keymap fonts for all-feature tests

* ci: provision PSD corpus for full workspace tests
This commit is contained in:
Dylan M. Taylor authored and GitHub committed 2026-11-13 02:17:30 -05:00
1 parent 72d544b707
commit 47107f6601
12 files changed
+65 -14

No files matched your search

+6
View File
@@ -8,3 +8,9 @@ crates/font-store/index/indexes/*.gz binary
# Copies of the vendored krilla source diff, one per patch (see vendor/krilla/README.craft.md).
vendor/krilla/patches/*.patch linguist-generated=true
# Has a pinned digest and is embedded as bytes by craft-font-store.
crates/font-store/index/sources.json -text
# The build fixture verifies these license files are copied byte-for-byte.
crates/fonts-build/tests/fixture/craft-fonts/fonts/**/OFL.txt -text
+1 -1
View File
@@ -413,7 +413,7 @@ impl<'a> Native<'a> {
let Some(next) = self.packets.next() else {
// A track that declares no frames (an empty WAV or AIFF) is empty, not broken.
let empty = self.frames_known && self.info.frames == 0 && self.errors == 0;
if !self.decoded && !empty && !(self.seeked && self.errors == 0) {
if !(self.decoded || empty || self.seeked && self.errors == 0) {
return Err(self.no_audio("stream contains no audio"));
}
return Ok(None);
+1 -1
View File
@@ -443,7 +443,7 @@ fn read_axes<'a>(c: &mut Chart, pa: &'a El, primary_val: &Option<String>) -> (Op
// A gridline whose `c:spPr/a:ln` has `a:noFill` is not drawn, though the element is there.
let grid = |a: &El, k: &str| {
a.child(Local(k))
.is_some_and(|g| !g.child(Local("spPr")).and_then(|p| p.child(Local("ln"))).is_some_and(|l| l.child(Local("noFill")).is_some()))
.is_some_and(|g| g.child(Local("spPr")).and_then(|p| p.child(Local("ln"))).is_none_or(|l| l.child(Local("noFill")).is_none()))
};
if c.kind == ChartType::Combo && c.series.iter().any(|s| s.secondary_axis) {
let primary_id = val_ax.and_then(|a| a.child(Local("axId")).and_then(|c| c.attr_local("val")));
+1 -1
View File
@@ -471,7 +471,7 @@ impl Loaded {
/// notes why not otherwise.
fn add(&mut self, name: &str, overrides: Option<Overrides>) {
let shown = || name.chars().take(MAX_SET_NAME).collect::<String>();
if !set_name(name).is_some_and(|n| n == name) {
if set_name(name).is_none_or(|n| n != name) {
self.note(SetProblem::BadName { name: shown() });
} else if self.sets.contains_key(name) {
self.note(SetProblem::Duplicate { name: shown() });
+13
View File
@@ -563,6 +563,19 @@ fn materials(j: &Value, limits: &Limits, scene: &mut Scene) -> Result<()> {
_ => Wrap::Repeat,
};
let images = scene.images.len();
let texture_list = arr(j, "textures");
if texture_list.len() > limits.max_materials {
return Err(too_large(KIND, format!("more than {} textures", limits.max_materials)));
}
scene.texture_variants = texture_list
.iter()
.filter_map(|texture| {
let sampler = idx(texture, "sampler").and_then(|s| arr(j, "samplers").get(s));
let webp = || texture.get("extensions").and_then(|e| e.get("EXT_texture_webp")).and_then(|e| idx(e, "source"));
let image = idx(texture, "source").or_else(webp).filter(|i| *i < images)?;
Some(TextureRef { image, uv_set: 0, wrap: [wrap(sampler, "wrapS"), wrap(sampler, "wrapT")] })
})
.collect();
let tref = |v: Option<&Value>| -> Option<TextureRef> {
let v = v?;
let texture = arr(j, "textures").get(idx(v, "index")?)?;
+6
View File
@@ -621,6 +621,9 @@ pub struct Scene {
pub meshes: Vec<Mesh>,
pub materials: Vec<Material>,
pub images: Vec<Image>,
/// Texture and sampler entries in the file, including ones no material uses. Their image
/// index and wrap modes are format properties; UV sets belong to each material slot.
pub texture_variants: Vec<TextureRef>,
/// Every format fills this: a format without a hierarchy gets one root node per mesh.
pub nodes: Vec<Node>,
/// The top-level nodes.
@@ -684,6 +687,9 @@ impl Scene {
return bad(format!("material `{}` names an image that does not exist", m.name));
}
}
if self.texture_variants.iter().any(|t| t.image >= self.images.len()) {
return bad("a texture names an image that does not exist".into());
}
let n = self.nodes.len();
for node in &self.nodes {
let refs =
+5
View File
@@ -79,6 +79,11 @@ fn validate_catches_what_writers_cannot_take() {
s.validate().unwrap();
s.meshes[0].parts[0].material = Some(0);
assert!(s.validate().is_err());
let mut s = Scene::default();
s.texture_variants.push(TextureRef::default());
assert!(s.validate().is_err(), "a texture references a missing image");
s.images.push(Image::default());
s.validate().unwrap();
let mut s = Scene::from_meshes(vec![tri_mesh()], vec![]);
s.nodes[0].children.push(7);
assert!(s.validate().is_err());
+24
View File
@@ -194,6 +194,30 @@ fn embedded_and_external_resources() {
assert!(parse(&gltf_text("data:;base64,!!!", "x.png").0).is_err());
}
#[test]
fn texture_and_sampler_inventory_includes_unreferenced_entries() {
let doc = serde_json::to_vec(&json!({
"asset": {"version": "2.0"},
"images": [{"uri": "image.png"}],
"samplers": [{"wrapS": 33071, "wrapT": 33648}],
"textures": [
{"source": 0, "sampler": 0},
{"source": 0}
]
}))
.unwrap();
let scene = read(&doc, &Limits::default(), &mut |_| Some(vec![])).unwrap();
assert_eq!(
scene.texture_variants,
[TextureRef { image: 0, uv_set: 0, wrap: [Wrap::Clamp, Wrap::Mirror] }, TextureRef { image: 0, uv_set: 0, wrap: [Wrap::Repeat; 2] },]
);
assert!(scene.validate().is_ok());
let mut invalid = scene;
invalid.texture_variants[0].image = invalid.images.len();
assert!(matches!(invalid.validate(), Err(Error::Malformed(_))));
}
#[test]
fn base64_and_uri_decoding() {
assert_eq!(base64_decode("aGVsbG8=").unwrap(), b"hello");
+3 -3
View File
@@ -8,10 +8,10 @@ fn scenes() -> Vec<Scene> {
scene::load_dir(&scene::scenes_dir()).unwrap()
}
/// On CI (`CI` set) the workflow provisions the font, so a scene that left text out fails.
/// Local runs without the font skip text and only note it.
/// When the licensed fixture directory is configured, every scene must write its text.
/// Workflows that do not provision those fixtures can still validate scenes without text.
fn text_must_be_written() -> bool {
std::env::var_os("CI").is_some_and(|v| !v.is_empty() && v != "false")
std::env::var_os(reference::FONT_DIR_ENV).is_some_and(|v| !v.is_empty())
}
fn missing_font(scene: &Scene) -> String {
+1 -1
View File
@@ -242,7 +242,7 @@ fn score(enc: &'static Encoding, name: &[u8], hint_letters: &std::collections::B
// never writes this one, says the writer meant that ("état", not "Žtat").
let written = |c: char| c.to_lowercase().all(|l| hint_letters.contains(&l));
let mac = name.get(i..=i).map(|b| decode(b, MACINTOSH)).and_then(|s| s.chars().next());
word_initial && !(mac.is_some_and(|m| m.is_alphabetic() && written(m)) && !written(ch))
word_initial && (written(ch) || !mac.is_some_and(|m| m.is_alphabetic() && written(m)))
} else if enc == WINDOWS_1251 && is_cyrillic(ch) {
(cyrillic_name && !lone_after_digit(&decoded, i)) || cyrillic_word(&decoded, i)
} else {
+1 -1
View File
@@ -117,7 +117,7 @@ impl<P: Clone + Eq + Hash + Debug + Send + Sync + 'static> Renderer<'_, P> {
// release outside the window floats.
let gone = !has_pointer && !touch_ended;
let floats_outside = !self.style.float_on_empty_drop && drag.gesture != GroupGesture::Window;
if !(released && !gone) && !(gone && floats_outside) {
if !(released && !gone || gone && floats_outside) {
return;
}
let Some(pointer) = pointer.filter(|p| !self.blockers.iter().any(|r| r.contains(*p))) else { return };
+3 -6
View File
@@ -598,7 +598,7 @@ fn resolve_pending(
st.unsupported(TOO_MUCH);
continue;
}
if !st.formula_at(p.pos).is_some_and(|f| f.text.chars().count() <= MAX_SHARED_TEMPLATE) {
if st.formula_at(p.pos).is_none_or(|f| f.text.chars().count() > MAX_SHARED_TEMPLATE) {
failed.insert(p.anchor);
}
} else {
@@ -902,11 +902,8 @@ fn sheet_record(rec: &Record<'_>, st: &mut SheetState<'_>, budget: &mut Budget,
report.warn(Warning::ConditionalFormattingSkipped);
}
},
0x01B1 => {
if conditional_format(&mut c, st).is_none() {
report.warn(Warning::ConditionalFormattingSkipped);
}
}
0x01B1 if conditional_format(&mut c, st).is_none() => report.warn(Warning::ConditionalFormattingSkipped),
0x01B1 => {}
0x0879 => report.warn(Warning::ConditionalFormatting12),
// A short DVal starts no rules.
0x01B2 => st.dv_left = crate::dv::dval(&mut c).unwrap_or(0),