diff --git a/assets/ATTRIBUTION.md b/assets/ATTRIBUTION.md index 9c83589d1..334850876 100644 --- a/assets/ATTRIBUTION.md +++ b/assets/ATTRIBUTION.md @@ -2,19 +2,21 @@ Every asset shipped in this repository is listed here (see the asset rules in `AGENTS.md`). No asset may come from Adobe products. Add an entry in the same commit as the asset. +`cargo xtask assets` (part of `cargo xtask ci`) fails if any image, icon, font, sound, video, raw file or colour profile +in the repository is not matched by a path pattern in the first column (`*` = any characters except `/`; a trailing `/` +covers a whole directory), or if a licence file referenced in the Licence column is missing. | Path | Asset | Creator | Source | Licence | Added | Modifications | |---|---|---|---|---|---|---| -| `assets/fonts/SourceSans3-Regular.ttf`, `-Semibold.ttf`, `-Bold.ttf`, `-It.ttf` | Source Sans 3 | Paul D. Hunt | https://github.com/adobe-fonts/source-sans | SIL Open Font License 1.1 (`assets/fonts/OFL-SourceSans3.txt`) | 2026-09-30 | none | -| `assets/fonts/Inter-Regular.ttf`, `-Medium.ttf`, `-SemiBold.ttf` | Inter | Rasmus Andersson | https://github.com/rsms/inter | SIL Open Font License 1.1 (`assets/fonts/OFL-Inter.txt`) | 2026-09-30 | none | +| `assets/fonts/Inter-*.ttf` (Regular, Medium, SemiBold) | Inter | Rasmus Andersson | https://github.com/rsms/inter | SIL Open Font License 1.1 (`assets/fonts/OFL-Inter.txt`) | 2026-09-30 | none | | `crates/scenes/` (generated images) | Procedural demo photographs | LightCraft contributors | original work (generated at runtime, no source imagery) | MIT OR Apache-2.0 | 2026-09-30 | n/a | | `crates/ui-egui/src/icons.rs` | UI icons drawn as vector paths in code | LightCraft contributors | original work | MIT OR Apache-2.0 | 2026-09-30 | n/a | -| `docs/images/*-tetons.jpg`, `grid-pd.jpg` (screenshots containing it) | "The Tetons and the Snake River" | Ansel Adams (1942), U.S. National Archives | https://commons.wikimedia.org/wiki/File:Adams_The_Tetons_and_the_Snake_River.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | -| `docs/images/*-migrant-mother.jpg`, `grid-pd.jpg` | "Migrant Mother" | Dorothea Lange (1936), Farm Security Administration / Library of Congress | https://commons.wikimedia.org/wiki/File:Lange-MigrantMother02.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | -| `docs/images/*-earthrise.jpg`, `grid-pd.jpg` | "Earthrise" (AS08-14-2383) | NASA / Bill Anders, Apollo 8 (1968) | https://commons.wikimedia.org/wiki/File:NASA-Apollo8-Dec24-Earthrise.jpg | Public domain (NASA) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | -| `docs/images/*-blue-marble.jpg`, `grid-pd.jpg` | "The Blue Marble" (AS17-148-22727) | NASA, Apollo 17 crew (1972) | https://commons.wikimedia.org/wiki/File:The_Earth_seen_from_Apollo_17.jpg | Public domain (NASA) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | +| `docs/images/*-tetons.jpg`, `docs/images/grid-pd.jpg` (screenshots containing it) | "The Tetons and the Snake River" | Ansel Adams (1942), U.S. National Archives | https://commons.wikimedia.org/wiki/File:Adams_The_Tetons_and_the_Snake_River.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | +| `docs/images/*-migrant-mother.jpg`, `docs/images/grid-pd.jpg` | "Migrant Mother" | Dorothea Lange (1936), Farm Security Administration / Library of Congress | https://commons.wikimedia.org/wiki/File:Lange-MigrantMother02.jpg | Public domain (U.S. federal government work) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | +| `docs/images/*-earthrise.jpg`, `docs/images/grid-pd.jpg` | "Earthrise" (AS08-14-2383) | NASA / Bill Anders, Apollo 8 (1968) | https://commons.wikimedia.org/wiki/File:NASA-Apollo8-Dec24-Earthrise.jpg | Public domain (NASA) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | +| `docs/images/*-blue-marble.jpg`, `docs/images/grid-pd.jpg` | "The Blue Marble" (AS17-148-22727) | NASA, Apollo 17 crew (1972) | https://commons.wikimedia.org/wiki/File:The_Earth_seen_from_Apollo_17.jpg | Public domain (NASA) | 2026-09-30 | developed in LightCraft; shown inside app screenshots | | `docs/images/*.jpg` (UI) | LightCraft application screenshots | LightCraft contributors | original work (captured with `docs/showcase/`) | MIT OR Apache-2.0 | 2026-09-30 | n/a | Notes -- Source Sans 3 is an open-source (OFL) typeface; it is used as a general-purpose UI font. It is distributed under the - OFL and is not an asset taken from any Adobe application. +- Fonts authored or published by Adobe (Source Sans/Serif/Code, Source Han, …) are not used, even though some are + OFL-licensed: the asset rule excludes anything from Adobe. Source Sans 3 was removed on 2026-09-30 and replaced by Inter. diff --git a/assets/fonts/OFL-SourceSans3.txt b/assets/fonts/OFL-SourceSans3.txt deleted file mode 100644 index 22c601b82..000000000 --- a/assets/fonts/OFL-SourceSans3.txt +++ /dev/null @@ -1,93 +0,0 @@ -Copyright 2010-2024 Adobe (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe in the United States and/or other countries. - -This Font Software is licensed under the SIL Open Font License, Version 1.1. - -This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL - - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION & CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/assets/fonts/SourceSans3-Bold.ttf b/assets/fonts/SourceSans3-Bold.ttf deleted file mode 100644 index 85693ccc8..000000000 Binary files a/assets/fonts/SourceSans3-Bold.ttf and /dev/null differ diff --git a/assets/fonts/SourceSans3-It.ttf b/assets/fonts/SourceSans3-It.ttf deleted file mode 100644 index 8e0576f24..000000000 Binary files a/assets/fonts/SourceSans3-It.ttf and /dev/null differ diff --git a/assets/fonts/SourceSans3-Regular.ttf b/assets/fonts/SourceSans3-Regular.ttf deleted file mode 100644 index c5dd0b88c..000000000 Binary files a/assets/fonts/SourceSans3-Regular.ttf and /dev/null differ diff --git a/assets/fonts/SourceSans3-Semibold.ttf b/assets/fonts/SourceSans3-Semibold.ttf deleted file mode 100644 index 4aff9a3ae..000000000 Binary files a/assets/fonts/SourceSans3-Semibold.ttf and /dev/null differ diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 4c16dc90a..464bdbbd1 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -105,7 +105,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context) { Dialog::About => { ui.label(egui::RichText::new("LightCraft").font(t.semibold(20.0)).color(t.text)); ui.label(format!("Version {} — a clean-room, pure-Rust photo library and raw developer.", env!("CARGO_PKG_VERSION"))); - ui.label("MIT OR Apache-2.0. Fonts: Source Sans 3 (OFL). Icons: original."); + ui.label("MIT OR Apache-2.0. Font: Inter (OFL). Icons: original."); } Dialog::Shortcuts => { egui::ScrollArea::vertical().max_height(420.0).show(ui, |ui| { diff --git a/crates/ui-egui/src/theme.rs b/crates/ui-egui/src/theme.rs index 4d8889056..c2990a2b9 100644 --- a/crates/ui-egui/src/theme.rs +++ b/crates/ui-egui/src/theme.rs @@ -107,15 +107,13 @@ impl Tokens { pub fn install_fonts(ctx: &egui::Context) { let mut fonts = FontDefinitions::default(); - fonts.font_data.insert("SourceSans3".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/SourceSans3-Regular.ttf")))); - fonts - .font_data - .insert("SourceSans3-Semibold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/SourceSans3-Semibold.ttf")))); + fonts.font_data.insert("Inter".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/Inter-Regular.ttf")))); + fonts.font_data.insert("Inter-SemiBold".into(), Arc::new(FontData::from_static(include_bytes!("../../../assets/fonts/Inter-SemiBold.ttf")))); let fallback: Vec = fonts.families.get(&FontFamily::Proportional).cloned().unwrap_or_default(); - let mut prop = vec!["SourceSans3".to_string()]; + let mut prop = vec!["Inter".to_string()]; prop.extend(fallback.clone()); fonts.families.insert(FontFamily::Proportional, prop); - let mut semi = vec!["SourceSans3-Semibold".to_string()]; + let mut semi = vec!["Inter-SemiBold".to_string()]; semi.extend(fallback); fonts.families.insert(FontFamily::Name(FONT_SEMIBOLD.into()), semi); ctx.set_fonts(fonts); diff --git a/xtask/src/assets.rs b/xtask/src/assets.rs new file mode 100644 index 000000000..7a932945d --- /dev/null +++ b/xtask/src/assets.rs @@ -0,0 +1,135 @@ +//! Asset attribution check (AGENTS.md "Assets" rule): every image, icon, font, sound, video, raw +//! file or colour profile in the repository must be covered by a path pattern in the first column of +//! `assets/ATTRIBUTION.md`, licence files referenced there must exist, and nothing may look like it +//! came from an Adobe product. + +use std::path::Path; +use std::process::Command; + +/// Extensions treated as assets (lower-case). +const ASSET_EXT: &[&str] = &[ + "png", "jpg", "jpeg", "gif", "webp", "avif", "svg", "ico", "icns", "bmp", "ttf", "otf", "woff", "woff2", "wav", "mp3", "ogg", "flac", "mp4", + "mov", "tif", "tiff", "dng", "cr2", "cr3", "nef", "arw", "raf", "orf", "rw2", "pef", "heic", "jxl", "psd", "cube", "icc", "icm", "xmp", +]; +/// Adobe-specific formats we never ship (camera/lens profiles, Lightroom templates). +const FORBIDDEN_EXT: &[&str] = &["dcp", "lcp", "lrtemplate", "lrcat", "lrsmcol", "aco", "ase", "abr"]; +const FORBIDDEN_NAME: &[&str] = &["adobe", "lightroom", "photoshop", "creative cloud", "creativecloud"]; + +/// `*` matches any run of characters except `/`; a pattern ending in `/` matches everything below it. +pub fn glob(pat: &str, path: &str) -> bool { + if let Some(dir) = pat.strip_suffix('/') { + return path.starts_with(dir) && path[dir.len()..].starts_with('/'); + } + fn rec(p: &[u8], s: &[u8]) -> bool { + match (p.first(), s.first()) { + (None, None) => true, + (Some(b'*'), _) => rec(&p[1..], s) || (!s.is_empty() && s[0] != b'/' && rec(p, &s[1..])), + (Some(a), Some(b)) if a == b => rec(&p[1..], &s[1..]), + _ => false, + } + } + rec(pat.as_bytes(), path.as_bytes()) +} + +fn backticked(cell: &str) -> impl Iterator { + cell.split('`').skip(1).step_by(2) +} + +/// (path patterns, referenced licence files) from the attribution table. +pub fn parse(md: &str) -> (Vec, Vec) { + let (mut pats, mut licences) = (Vec::new(), Vec::new()); + for line in md.lines().filter(|l| l.trim_start().starts_with('|')) { + let cells: Vec<&str> = line.trim().trim_matches('|').split('|').collect(); + if cells.len() < 5 { + continue; + } + pats.extend(backticked(cells[0]).filter(|p| p.contains('/')).map(str::to_string)); + licences.extend(backticked(cells[4]).filter(|p| p.contains('/')).map(str::to_string)); + } + (pats, licences) +} + +/// Problems with `files` given the attribution document `md`; `exists` checks licence files. +pub fn check(files: &[String], md: &str, exists: impl Fn(&str) -> bool) -> Vec { + let (pats, licences) = parse(md); + let mut out = Vec::new(); + for f in files { + let lower = f.to_ascii_lowercase(); + let ext = Path::new(&lower).extension().and_then(|e| e.to_str()).unwrap_or(""); + let name = Path::new(&lower).file_name().and_then(|e| e.to_str()).unwrap_or(""); + if FORBIDDEN_EXT.contains(&ext) { + out.push(format!("{f}: `.{ext}` files (Adobe profile/template formats) must never be committed")); + continue; + } + if !ASSET_EXT.contains(&ext) { + continue; + } + if FORBIDDEN_NAME.iter().any(|n| name.contains(n)) { + out.push(format!("{f}: asset name suggests Adobe material; Adobe assets are forbidden (AGENTS.md)")); + } + if !pats.iter().any(|p| glob(p, f)) { + out.push(format!("{f}: no entry in assets/ATTRIBUTION.md (add path, creator, source, licence, date, modifications)")); + } + } + for l in licences { + if !exists(&l) { + out.push(format!("assets/ATTRIBUTION.md references missing licence file `{l}`")); + } + } + out +} + +pub fn run(root: &Path) -> Result<(), String> { + let out = Command::new("git") + .args(["ls-files", "--cached", "--others", "--exclude-standard"]) + .current_dir(root) + .output() + .map_err(|e| format!("git ls-files: {e}"))?; + if !out.status.success() { + return Err("git ls-files failed".into()); + } + let files: Vec = String::from_utf8_lossy(&out.stdout).lines().filter(|f| root.join(f).exists()).map(str::to_string).collect(); + let md = std::fs::read_to_string(root.join("assets/ATTRIBUTION.md")).map_err(|e| format!("assets/ATTRIBUTION.md: {e}"))?; + let problems = check(&files, &md, |p| root.join(p).exists()); + if problems.is_empty() { + let n = files + .iter() + .filter(|f| Path::new(&f.to_ascii_lowercase()).extension().and_then(|e| e.to_str()).is_some_and(|e| ASSET_EXT.contains(&e))) + .count(); + eprintln!("assets: {n} asset files, all attributed"); + Ok(()) + } else { + Err(format!("asset rule violations:\n {}", problems.join("\n "))) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const MD: &str = "| Path | Asset | Creator | Source | Licence | Added | Mods |\n|---|---|---|---|---|---|---|\n\ + | `assets/fonts/Foo-*.ttf` | Foo | A | url | OFL (`assets/fonts/OFL-Foo.txt`) | d | none |\n\ + | `crates/scenes/` | gen | us | original | MIT | d | n/a |\n"; + + #[test] + fn globbing() { + assert!(glob("docs/images/*-tetons.jpg", "docs/images/ba-tetons.jpg")); + assert!(!glob("docs/images/*.jpg", "docs/images/sub/x.jpg")); + assert!(glob("crates/scenes/", "crates/scenes/a/b.png")); + assert!(!glob("crates/scenes/", "crates/scenesx/b.png")); + } + + #[test] + fn flags_unattributed_forbidden_and_missing_licence() { + let files: Vec = + ["assets/fonts/Foo-Bold.ttf", "assets/icons/x.svg", "crates/scenes/t.png", "src/lib.rs", "p/camera.dcp", "assets/fonts/Adobe-Foo.otf"] + .map(String::from) + .into(); + let p = check(&files, MD, |_| false); + assert!(p.iter().any(|m| m.starts_with("assets/icons/x.svg: no entry"))); + assert!(p.iter().any(|m| m.starts_with("p/camera.dcp"))); + assert!(p.iter().any(|m| m.contains("Adobe-Foo.otf: asset name"))); + assert!(p.iter().any(|m| m.contains("OFL-Foo.txt"))); + assert!(!p.iter().any(|m| m.starts_with("assets/fonts/Foo-Bold.ttf") || m.starts_with("crates/scenes") || m.starts_with("src/"))); + } +} diff --git a/xtask/src/main.rs b/xtask/src/main.rs index 305d3dc49..914574621 100644 --- a/xtask/src/main.rs +++ b/xtask/src/main.rs @@ -3,6 +3,7 @@ //! Pure Rust (std + serde_json). External tools (`cargo`, `curl`, `tar`) are //! invoked through `std::process::Command`. +mod assets; mod layers; mod stats; @@ -13,9 +14,10 @@ const USAGE: &str = "\ usage: cargo xtask commands: + assets every image/icon/font/media file is attributed in assets/ATTRIBUTION.md; no Adobe assets layers enforce the crate dependency layering (plan/architecture.md §3) wasm cargo check --target wasm32-unknown-unknown for the wasm-safe crates - ci fmt --check, clippy -D warnings, test, layers, wasm (stops at first failure) + ci fmt --check, clippy -D warnings, test, layers, assets, wasm (stops at first failure) corpus [--download] show where test corpora live; --download fetches PngSuite into corpus/pngsuite stats [--exact] count tests and lines per crate (--exact: ask the test harness via `-- --list`) @@ -26,6 +28,7 @@ fn main() -> ExitCode { let rest: Vec<&str> = args.iter().skip(1).map(String::as_str).collect(); let result = match args.first().map(String::as_str) { Some("layers") => cmd_layers(), + Some("assets") => assets::run(&root()), Some("wasm") => cmd_wasm(), Some("ci") => cmd_ci(), Some("corpus") => cmd_corpus(rest.contains(&"--download")), @@ -164,6 +167,7 @@ fn cmd_ci() -> Result<(), String> { }), ), ("layers", Box::new(cmd_layers)), + ("assets", Box::new(|| assets::run(&root()))), ("wasm", Box::new(cmd_wasm)), ]; let mut done = Vec::new();