Add Rust logo pipeline: trim, metrics, knockout and Branding settings
Logos (PNG, JPEG, WebP) are decoded, trimmed to their ink box, downscaled to 1200 px and re-encoded as a content-addressed print PNG plus a white knockout variant when the logo has alpha; aspect, ink density, mean colour and kind (wordmark, mark, tall) are stored with the settings and frozen in the issue snapshot. Legacy logos are derived at startup. The Test Vendor logo PDF shrinks from 334 KB to 92 KB. Settings gains a Branding section with white and dark previews and a business-name toggle (migration M5).
This commit is contained in:
@@ -78,6 +78,24 @@ fn write_asset(data_dir: &Path, kind: &str, bytes: &[u8]) -> Result<String, Stri
|
||||
Ok(format!("assets/{file}"))
|
||||
}
|
||||
|
||||
/// Write `assets/<file_name>` unless it already exists. For content-addressed derivatives, where the
|
||||
/// same name always means the same bytes. The name must be a plain `[a-z0-9._-]` file name.
|
||||
pub fn write_named_asset(data_dir: &Path, file_name: &str, bytes: &[u8]) -> Result<String, String> {
|
||||
let plain = !file_name.is_empty()
|
||||
&& !file_name.starts_with('.')
|
||||
&& file_name
|
||||
.chars()
|
||||
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || matches!(c, '.' | '-' | '_'));
|
||||
if !plain {
|
||||
return Err(format!("Invalid asset name: {file_name}"));
|
||||
}
|
||||
let target = assets_dir(data_dir)?.join(file_name);
|
||||
if !target.exists() {
|
||||
std::fs::write(&target, bytes).map_err(|e| e.to_string())?;
|
||||
}
|
||||
Ok(format!("assets/{file_name}"))
|
||||
}
|
||||
|
||||
fn asset_data_uri(data_dir: &Path, path: &str) -> Result<String, String> {
|
||||
let file = resolve_asset(data_dir, path)?;
|
||||
let bytes = std::fs::read(&file).map_err(|e| e.to_string())?;
|
||||
@@ -162,6 +180,18 @@ mod tests {
|
||||
assert!(uri.starts_with("data:image/jpeg;base64,"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn named_assets_are_plain_names_and_not_overwritten() {
|
||||
let dir = tempdir().unwrap();
|
||||
let stored = write_named_asset(dir.path(), "logo-ab12cd34-print.png", b"one").unwrap();
|
||||
assert_eq!(stored, "assets/logo-ab12cd34-print.png");
|
||||
write_named_asset(dir.path(), "logo-ab12cd34-print.png", b"two").unwrap();
|
||||
assert_eq!(std::fs::read(dir.path().join(&stored)).unwrap(), b"one");
|
||||
for bad in ["", ".hidden", "../x.png", "a/b.png", "A.png", "x y.png"] {
|
||||
assert!(write_named_asset(dir.path(), bad, b"x").is_err(), "{bad}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn data_uri_mime_comes_from_the_bytes_not_the_extension() {
|
||||
let dir = tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user