diff --git a/.gitattributes b/.gitattributes index fa2751233..a854bbdb9 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/crates/audio/src/read/native.rs b/crates/audio/src/read/native.rs index cc0fac4c4..5502ba7e1 100644 --- a/crates/audio/src/read/native.rs +++ b/crates/audio/src/read/native.rs @@ -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); diff --git a/crates/drawingml/src/chart/read.rs b/crates/drawingml/src/chart/read.rs index 9919433e9..38ce9f605 100644 --- a/crates/drawingml/src/chart/read.rs +++ b/crates/drawingml/src/chart/read.rs @@ -443,7 +443,7 @@ fn read_axes<'a>(c: &mut Chart, pa: &'a El, primary_val: &Option) -> (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"))); diff --git a/crates/keymap/src/profiles.rs b/crates/keymap/src/profiles.rs index df9634db6..3db892978 100644 --- a/crates/keymap/src/profiles.rs +++ b/crates/keymap/src/profiles.rs @@ -471,7 +471,7 @@ impl Loaded { /// notes why not otherwise. fn add(&mut self, name: &str, overrides: Option) { let shown = || name.chars().take(MAX_SET_NAME).collect::(); - 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() }); diff --git a/crates/mesh/src/gltf.rs b/crates/mesh/src/gltf.rs index 44477ddfb..9d45a4453 100644 --- a/crates/mesh/src/gltf.rs +++ b/crates/mesh/src/gltf.rs @@ -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 { let v = v?; let texture = arr(j, "textures").get(idx(v, "index")?)?; diff --git a/crates/mesh/src/lib.rs b/crates/mesh/src/lib.rs index c4bf90661..59870ff31 100644 --- a/crates/mesh/src/lib.rs +++ b/crates/mesh/src/lib.rs @@ -621,6 +621,9 @@ pub struct Scene { pub meshes: Vec, pub materials: Vec, pub images: Vec, + /// 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, /// Every format fills this: a format without a hierarchy gets one root node per mesh. pub nodes: Vec, /// 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 = diff --git a/crates/mesh/src/tests.rs b/crates/mesh/src/tests.rs index 587ef4db1..160b5b3dc 100644 --- a/crates/mesh/src/tests.rs +++ b/crates/mesh/src/tests.rs @@ -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()); diff --git a/crates/mesh/src/tests/gltf.rs b/crates/mesh/src/tests/gltf.rs index 011f4d75e..d35b63d57 100644 --- a/crates/mesh/src/tests/gltf.rs +++ b/crates/mesh/src/tests/gltf.rs @@ -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"); diff --git a/crates/plate-test/tests/scenes.rs b/crates/plate-test/tests/scenes.rs index 0e114471e..8b23e72d8 100644 --- a/crates/plate-test/tests/scenes.rs +++ b/crates/plate-test/tests/scenes.rs @@ -8,10 +8,10 @@ fn scenes() -> Vec { 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 { diff --git a/crates/psd/src/codepage.rs b/crates/psd/src/codepage.rs index 2ef7389ee..8cfe169d1 100644 --- a/crates/psd/src/codepage.rs +++ b/crates/psd/src/codepage.rs @@ -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 { diff --git a/crates/ui/src/docking/render/preview.rs b/crates/ui/src/docking/render/preview.rs index b3cb62488..8cd1848b2 100644 --- a/crates/ui/src/docking/render/preview.rs +++ b/crates/ui/src/docking/render/preview.rs @@ -117,7 +117,7 @@ impl 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 }; diff --git a/crates/xls/src/read.rs b/crates/xls/src/read.rs index 529a36e11..8fdf5e473 100644 --- a/crates/xls/src/read.rs +++ b/crates/xls/src/read.rs @@ -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),