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 Arun P 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:
+47
-2
@@ -10,7 +10,7 @@ const MAX_BACKUPS: usize = 10;
|
||||
const BACKUP_PREFIX: &str = "voiced-pre-v";
|
||||
|
||||
/// Highest schema version, i.e. the number of entries in `migrations()`.
|
||||
const LATEST_VERSION: i64 = 4;
|
||||
const LATEST_VERSION: i64 = 5;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum DbError {
|
||||
@@ -181,8 +181,17 @@ ALTER TABLE invoices ADD COLUMN archived_at TEXT;
|
||||
ALTER TABLE app_settings ADD COLUMN last_export_dir TEXT NOT NULL DEFAULT '';
|
||||
"#;
|
||||
|
||||
/// Version 5: logo pipeline outputs. `logo_path` now points at the trimmed print image; the original
|
||||
/// is kept so it can be re-derived. Invoice snapshots issued earlier simply lack the new keys.
|
||||
const M5: &str = r#"
|
||||
ALTER TABLE app_settings ADD COLUMN logo_original_path TEXT;
|
||||
ALTER TABLE app_settings ADD COLUMN logo_knockout_path TEXT;
|
||||
ALTER TABLE app_settings ADD COLUMN logo_meta TEXT NOT NULL DEFAULT '';
|
||||
ALTER TABLE app_settings ADD COLUMN logo_includes_name TEXT NOT NULL DEFAULT 'auto';
|
||||
"#;
|
||||
|
||||
fn migrations() -> Migrations<'static> {
|
||||
Migrations::new(vec![M::up(SCHEMA), M::up(M2), M::up(M3), M::up(M4)])
|
||||
Migrations::new(vec![M::up(SCHEMA), M::up(M2), M::up(M3), M::up(M4), M::up(M5)])
|
||||
}
|
||||
|
||||
/// Open (creating if needed) the database at `path` and bring it to the latest schema.
|
||||
@@ -585,6 +594,42 @@ CREATE INDEX IF NOT EXISTS idx_invoices_created ON invoices(created_at DESC);
|
||||
assert_eq!(stored, json);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn logo_columns_default_and_existing_logo_is_untouched() {
|
||||
let conn = open_in_memory().unwrap();
|
||||
let row: (Option<String>, Option<String>, String, String) = conn
|
||||
.query_row(
|
||||
"SELECT logo_original_path, logo_knockout_path, logo_meta, logo_includes_name
|
||||
FROM app_settings WHERE id = 1",
|
||||
[],
|
||||
|r| Ok((r.get(0)?, r.get(1)?, r.get(2)?, r.get(3)?)),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(row, (None, None, String::new(), "auto".to_string()));
|
||||
|
||||
// A database at v4 with a logo keeps it; only the new columns appear.
|
||||
let dir = tempdir().unwrap();
|
||||
let path = dir.path().join("voiced.db");
|
||||
{
|
||||
let mut old = Connection::open(&path).unwrap();
|
||||
Migrations::new(vec![M::up(SCHEMA), M::up(M2), M::up(M3), M::up(M4)])
|
||||
.to_latest(&mut old)
|
||||
.unwrap();
|
||||
seed(&old).unwrap();
|
||||
old.execute("UPDATE app_settings SET logo_path = 'assets/logo-old.png'", []).unwrap();
|
||||
assert_eq!(user_version(&old), 4);
|
||||
}
|
||||
let conn = open(&path, &dir.path().join("backups")).unwrap();
|
||||
assert_eq!(user_version(&conn), 5);
|
||||
let (logo, meta): (String, String) = conn
|
||||
.query_row("SELECT logo_path, logo_meta FROM app_settings WHERE id = 1", [], |r| {
|
||||
Ok((r.get(0)?, r.get(1)?))
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!((logo.as_str(), meta.as_str()), ("assets/logo-old.png", ""));
|
||||
assert_eq!(backups_in(&dir.path().join("backups")).len(), 1);
|
||||
}
|
||||
|
||||
fn build_v1_database(path: &Path) {
|
||||
let conn = Connection::open(path).unwrap();
|
||||
conn.execute_batch(V1_SCHEMA).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user