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:
2026-10-04 06:31:30 +05:30
parent 3c5fd617cd
commit e1edf02448
27 changed files with 1615 additions and 37 deletions
+45 -2
View File
@@ -6,7 +6,8 @@ use tauri::State;
pub(crate) const SETTINGS_COLS: &str = "vendor_name, vendor_address, vendor_email, vendor_phone, vendor_pan,
vendor_gstin, vendor_state_code, logo_path, signature_path, default_bank_id, default_tax_rate,
default_tax_type, payment_terms_days, currency, onboarded, theme, gst_registration, default_hsn_sac, signatory_name,
signatory_designation, render_prefs, last_export_dir";
signatory_designation, render_prefs, last_export_dir, logo_original_path, logo_knockout_path, logo_meta,
logo_includes_name";
pub(crate) fn map_settings(row: &Row) -> rusqlite::Result<Settings> {
Ok(Settings {
@@ -32,6 +33,10 @@ pub(crate) fn map_settings(row: &Row) -> rusqlite::Result<Settings> {
signatory_designation: row.get(19)?,
render_prefs: row.get(20)?,
last_export_dir: row.get(21)?,
logo_original_path: row.get(22)?,
logo_knockout_path: row.get(23)?,
logo_meta: row.get(24)?,
logo_includes_name: row.get(25)?,
})
}
@@ -48,6 +53,9 @@ pub fn get_settings(state: State<AppState>) -> Result<Settings, String> {
#[tauri::command]
pub fn save_settings(state: State<AppState>, settings: Settings) -> Result<Settings, String> {
if !matches!(settings.logo_includes_name.as_str(), "auto" | "yes" | "no") {
return Err("Logo includes name must be auto, yes or no".into());
}
let conn = state.db.lock().map_err(|e| e.to_string())?;
conn.execute(
r#"UPDATE app_settings SET
@@ -56,7 +64,9 @@ pub fn save_settings(state: State<AppState>, settings: Settings) -> Result<Setti
signature_path = ?9, default_bank_id = ?10, default_tax_rate = ?11,
default_tax_type = ?12, payment_terms_days = ?13, currency = ?14,
onboarded = ?15, theme = ?16, gst_registration = ?17, default_hsn_sac = ?18,
signatory_name = ?19, signatory_designation = ?20, render_prefs = ?21
signatory_name = ?19, signatory_designation = ?20, render_prefs = ?21,
logo_original_path = ?22, logo_knockout_path = ?23, logo_meta = ?24,
logo_includes_name = ?25
WHERE id = 1"#,
params![
settings.vendor_name,
@@ -80,6 +90,10 @@ pub fn save_settings(state: State<AppState>, settings: Settings) -> Result<Setti
settings.signatory_name,
settings.signatory_designation,
settings.render_prefs,
settings.logo_original_path,
settings.logo_knockout_path,
settings.logo_meta,
settings.logo_includes_name,
],
)
.map_err(|e| e.to_string())?;
@@ -253,6 +267,35 @@ mod tests {
assert_eq!(json["renderPrefs"], "{\"version\":1}");
}
#[test]
fn settings_carry_logo_columns() {
let conn = crate::db::open_in_memory().unwrap();
let read = |conn: &Connection| {
conn.query_row(
&format!("SELECT {} FROM app_settings WHERE id = 1", SETTINGS_COLS),
[],
map_settings,
)
.unwrap()
};
let fresh = read(&conn);
assert_eq!(fresh.logo_original_path, None);
assert_eq!(fresh.logo_knockout_path, None);
assert_eq!(fresh.logo_meta, "");
assert_eq!(fresh.logo_includes_name, "auto");
conn.execute(
"UPDATE app_settings SET logo_original_path = 'assets/o.png', logo_knockout_path = 'assets/k.png',
logo_meta = '{}', logo_includes_name = 'no' WHERE id = 1",
[],
)
.unwrap();
let json = serde_json::to_value(read(&conn)).unwrap();
assert_eq!(json["logoOriginalPath"], "assets/o.png");
assert_eq!(json["logoKnockoutPath"], "assets/k.png");
assert_eq!(json["logoMeta"], "{}");
assert_eq!(json["logoIncludesName"], "no");
}
#[test]
fn deleting_the_default_bank_promotes_the_lowest_remaining() {
let mut conn = crate::db::open_in_memory().unwrap();