From 227d0422da0062410c85529bc999d7e509d9de78 Mon Sep 17 00:00:00 2001 From: Anders Betts Date: Tue, 22 Sep 2026 09:04:27 +0200 Subject: documents: org name in the invoice and payslip headers, document_header_color --- src/cmd_invoices.c | 5 +++++ src/cmd_payroll.c | 4 ++++ src/cmd_settings.c | 17 ++++++++++++--- src/invoice.c | 16 ++++++++++---- src/invoice.h | 1 + src/payslip.c | 62 +++++++++--------------------------------------------- src/payslip.h | 1 + src/pdf.c | 36 +++++++++++++++++++++++++++++++ src/pdf.h | 6 ++++++ src/util.c | 13 ++++++++++++ src/util.h | 1 + 11 files changed, 103 insertions(+), 59 deletions(-) (limited to 'src') diff --git a/src/cmd_invoices.c b/src/cmd_invoices.c index 2a6a9c6..a9d3fb1 100644 --- a/src/cmd_invoices.c +++ b/src/cmd_invoices.c @@ -380,6 +380,7 @@ struct invoice_view { char seller_org_nr[64]; char seller_vat_nr[64]; char bankgiro[64]; + char header_color[8]; char customer_name[256]; char customer_address[1024]; char customer_postal[64]; @@ -467,6 +468,9 @@ static int invoice_view_fill(struct req *r, const struct invoice_draft *d, bankgiro && *bankgiro ? bankgiro : ""); free(bankgiro); + db_setting_copy(r->db, r->org_id, "document_header_color", "", + v->header_color, sizeof v->header_color); + v->lines = xcalloc(d->nlines, sizeof *v->lines); for (size_t i = 0; i < d->nlines; i++) { v->lines[i].article_no = d->lines[i].article_no; @@ -489,6 +493,7 @@ static int invoice_view_fill(struct req *r, const struct invoice_draft *d, v->doc.seller.org_nr = v->seller_org_nr; v->doc.seller.vat_nr = v->seller_vat_nr; v->doc.seller.bankgiro = v->bankgiro; + v->doc.header_color = v->header_color; v->doc.customer.name = v->customer_name; v->doc.customer.address = v->customer_address; v->doc.customer.postal_code = v->customer_postal; diff --git a/src/cmd_payroll.c b/src/cmd_payroll.c index 84ae323..6dea293 100644 --- a/src/cmd_payroll.c +++ b/src/cmd_payroll.c @@ -762,6 +762,7 @@ struct payslip_ctx { char employer_org_nr[64]; char employer_phone[64]; char employer_email[256]; + char header_color[8]; char filename[320]; int64_t voucher_id; int64_t employee_id; @@ -924,6 +925,8 @@ static int payslip_prepare(struct req *r, struct payslip_ctx *c) struct payroll_cfg cfg; payroll_cfg_load(r, &cfg); snprintf(c->run_ref, sizeof c->run_ref, "Lönekörning %s", c->period); + db_setting_copy(r->db, r->org_id, "document_header_color", "", + c->header_color, sizeof c->header_color); char safe_name[200]; snprintf(safe_name, sizeof safe_name, "%s", c->employee_name); @@ -942,6 +945,7 @@ static int payslip_prepare(struct req *r, struct payslip_ctx *c) c->d.employer.email = c->employer_email; c->d.employee_name = c->employee_name; c->d.personal_no_masked = c->personal_no; + c->d.header_color = c->header_color; c->d.period = c->period; c->d.pay_date = c->pay_date; c->d.run_ref = c->run_ref; diff --git a/src/cmd_settings.c b/src/cmd_settings.c index f41f904..5bac3d9 100644 --- a/src/cmd_settings.c +++ b/src/cmd_settings.c @@ -22,7 +22,7 @@ static yyjson_mut_val *h_settings_get(struct req *r) int have_default = 0, have_bank = 0, have_receivable = 0, have_revenue = 0; int have_password = 0, have_security = 0; int have_voucher = 0, have_invoice = 0, have_payroll = 0; - int have_bokslut = 0, have_ib = 0; + int have_bokslut = 0, have_ib = 0, have_header_color = 0; char legacy_series[16] = "A"; sqlite3_stmt *st = NULL; if (sqlite3_prepare_v2( @@ -59,6 +59,8 @@ static yyjson_mut_val *h_settings_get(struct req *r) have_receivable = 1; } else if (strcmp(k, "invoice_revenue_account") == 0) { have_revenue = 1; + } else if (strcmp(k, "document_header_color") == 0) { + have_header_color = 1; } else if (strcmp(k, "smtp_security") == 0) { have_security = 1; } @@ -87,6 +89,9 @@ static yyjson_mut_val *h_settings_get(struct req *r) if (!have_revenue) yyjson_mut_obj_add_strcpy(r->rdoc, o, "invoice_revenue_account", "3001"); + if (!have_header_color) + yyjson_mut_obj_add_strcpy(r->rdoc, o, "document_header_color", + "#314c59"); if (!have_security) yyjson_mut_obj_add_strcpy(r->rdoc, o, "smtp_security", "starttls"); yyjson_mut_obj_add_bool(r->rdoc, o, "smtp_password_set", have_password); @@ -162,7 +167,7 @@ static yyjson_mut_val *h_settings_set(struct req *r) return settings_result(r, key, "[redacted]", 0); } size_t maxlen; - int digits_only = 0, bankgiro = 0, port = 0, security = 0; + int digits_only = 0, bankgiro = 0, port = 0, security = 0, color = 0; if (strcmp(key, "default_series") == 0 || strcmp(key, "series_voucher") == 0 || strcmp(key, "series_invoice") == 0 || @@ -193,6 +198,9 @@ static yyjson_mut_val *h_settings_set(struct req *r) bankgiro = 1; } else if (strcmp(key, "invoice_our_ref") == 0) { maxlen = 64; + } else if (strcmp(key, "document_header_color") == 0) { + maxlen = 7; + color = 1; } else return fail(r, "UNSUPPORTED", "unknown setting"); size_t len = strlen(value); @@ -209,6 +217,8 @@ static yyjson_mut_val *h_settings_set(struct req *r) return failf(r, "INVALID_ARGS", "%s must not contain control characters", key); } + if (color && !util_hex_color_valid(value)) + return failf(r, "INVALID_ARGS", "%s must be #rrggbb", key); if (port) { long v = strtol(value, NULL, 10); if (v < 1 || v > 65535) @@ -252,7 +262,8 @@ static yyjson_mut_val *h_settings_set(struct req *r) static const struct cmd_arg args_settings_set[] = { { "key", ARG_STR, 1, NULL, NULL, "default_series, attachment_dir, bank_account," - " invoice_receivable_account, invoice_revenue_account, smtp_host," + " invoice_receivable_account, invoice_revenue_account," + " invoice_bankgiro, invoice_our_ref, document_header_color, smtp_host," " smtp_port, smtp_user, smtp_from, smtp_reply_to, smtp_security or" " smtp_password" }, { "value", ARG_STR, 0, NULL, NULL, diff --git a/src/invoice.c b/src/invoice.c index 34b2a85..1bdc0da 100644 --- a/src/invoice.c +++ b/src/invoice.c @@ -77,8 +77,12 @@ #define FOOT_CITY_Y 686.0238 #define FOOT_PAGE_Y 819.7954 -#define WORDMARK_X 21.742 +#define HEADER_NAME_X 21.742 #define FAKTURA_X 498.400 +#define HEADER_BASE_Y 69.1358 +#define HEADER_NAME_SIZE 16.0 +#define HEADER_NAME_MIN_SIZE 8.0 +#define HEADER_NAME_MAX_W 460.0 static void text_at(struct pdf *p, double x, double base, const char *font, double size, const char *rgb, const char *s) @@ -581,9 +585,13 @@ int invoice_render_pdf(const struct invoice_doc *d, unsigned char **out, p = pdf_new(); pdf_page(p); pdf_fill_rect(p, PAGE_LEFT, BAR_HEADER_Y, PAGE_RIGHT_X - PAGE_LEFT, - BAR_HEADER_H, INK); - draw_wordmark(p, WORDMARK_MAKANDRA, WORDMARK_X, 69.1358); - draw_wordmark(p, WORDMARK_FAKTURA, FAKTURA_X, 69.1358); + BAR_HEADER_H, + util_hex_color_valid(d->header_color) ? d->header_color + : INK); + pdf_text_fit(p, HEADER_NAME_X, HEADER_BASE_Y, "HB", HEADER_NAME_SIZE, + HEADER_NAME_MIN_SIZE, HEADER_NAME_MAX_W, WHITE, + d->seller.name); + draw_wordmark(p, WORDMARK_FAKTURA, FAKTURA_X, HEADER_BASE_Y); draw_info(p, d); draw_customer(p, &d->customer); draw_table(p, d); diff --git a/src/invoice.h b/src/invoice.h index 7d64f21..55d1e3b 100644 --- a/src/invoice.h +++ b/src/invoice.h @@ -41,6 +41,7 @@ struct invoice_doc { struct invoice_customer customer; int64_t number; const char *ocr; + const char *header_color; /* "#rrggbb"; NULL or invalid = default */ const char *invoice_date; const char *due_date; const char *delivery_date; diff --git a/src/payslip.c b/src/payslip.c index 81ebded..ee8e008 100644 --- a/src/payslip.c +++ b/src/payslip.c @@ -6,7 +6,6 @@ #include "pdf.h" #include "util.h" -#include "wordmark.h" #define INK "#314c59" #define WHITE "#ffffff" @@ -52,9 +51,12 @@ #define FOOT_PAGE_Y 819.7954 #define FOOT_PAGE_RIGHT 567.4275 -#define WORDMARK_X 21.742 +#define HEADER_NAME_X 21.742 #define TITLE_RIGHT 574.892 #define TITLE_BASE_Y 69.1358 +#define HEADER_NAME_SIZE 16.0 +#define HEADER_NAME_MIN_SIZE 8.0 +#define HEADER_NAME_MAX_W 460.0 static void text_at(struct pdf *p, double x, double base, const char *font, double size, const char *rgb, const char *s) @@ -72,54 +74,6 @@ static void text_right(struct pdf *p, double right, double base, rgb, s); } -/* pdf_path consumes SVG-style y-down paths; wordmark.h stores y-up outline - coordinates, so negate the y of every point before drawing. */ -static void draw_wordmark(struct pdf *p, const char *path, double x, double y) -{ - size_t n = strlen(path); - char *flipped = xmalloc(2 * n + 2); - char op = 0; - int num = 0, in_num = 0, y_coord = 0; - size_t o = 0; - - for (size_t i = 0; i < n; i++) { - char c = path[i]; - - if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) { - if (c == 'M' || c == 'L' || c == 'C') { - op = c; - num = 0; - } else if (c == 'Z') { - op = 0; - } - flipped[o++] = c; - in_num = 0; - continue; - } - if (!in_num && (c == '-' || c == '+' || c == '.' || - (c >= '0' && c <= '9'))) { - in_num = 1; - y_coord = op && (num % 2 == 1); - num++; - if (y_coord) { - if (c == '-') { - i++; - c = path[i]; - } else { - flipped[o++] = '-'; - } - } - } else if (!(c == '-' || c == '+' || c == '.' || - (c >= '0' && c <= '9'))) { - in_num = 0; - } - flipped[o++] = c; - } - flipped[o] = '\0'; - pdf_path(p, flipped, x, y, WHITE); - free(flipped); -} - static void group_digits(char *out, size_t n, uint64_t v) { char digits[24]; @@ -284,8 +238,12 @@ int payslip_render(const struct payslip_data *d, unsigned char **out, p = pdf_new(); pdf_page(p); pdf_fill_rect(p, PAGE_LEFT, BAR_HEADER_Y, PAGE_RIGHT_X - PAGE_LEFT, - BAR_HEADER_H, INK); - draw_wordmark(p, WORDMARK_MAKANDRA, WORDMARK_X, TITLE_BASE_Y); + BAR_HEADER_H, + util_hex_color_valid(d->header_color) ? d->header_color + : INK); + pdf_text_fit(p, HEADER_NAME_X, TITLE_BASE_Y, "HB", HEADER_NAME_SIZE, + HEADER_NAME_MIN_SIZE, HEADER_NAME_MAX_W, WHITE, + d->employer.name); text_right(p, TITLE_RIGHT, TITLE_BASE_Y, "HB", SIZE_TITLE, WHITE, "LÖNEBESKED"); pdf_line(p, PAGE_LEFT, RULE_HEADER_Y, PAGE_RIGHT_X, RULE_HEADER_Y, RULE_W, diff --git a/src/payslip.h b/src/payslip.h index 291b35d..581cd81 100644 --- a/src/payslip.h +++ b/src/payslip.h @@ -16,6 +16,7 @@ struct payslip_data { } employer; const char *employee_name; const char *personal_no_masked; + const char *header_color; /* "#rrggbb"; NULL or invalid = default */ const char *period; const char *pay_date; int tax_table; diff --git a/src/pdf.c b/src/pdf.c index c6ae876..5718ed7 100644 --- a/src/pdf.c +++ b/src/pdf.c @@ -353,6 +353,42 @@ double pdf_font_ascent(const char *font, double size) return size > 0 ? size * PDF_ASCENT / 1000.0 : 0.0; } +void pdf_text_fit(struct pdf *p, double x, double baseline, const char *font, + double size, double min_size, double max_w, const char *rgb, + const char *utf8) +{ + char *truncated = NULL; + const char *text = utf8; + double w; + + if (!utf8 || !*utf8 || max_w <= 0) + return; + w = pdf_text_width(font, size, utf8); + if (w > max_w) { + size = size * max_w / w; + if (size < min_size) + size = min_size; + w = pdf_text_width(font, size, utf8); + } + if (w > max_w) { + size_t n = strlen(utf8); + + truncated = xmalloc(n + 4); + memcpy(truncated, utf8, n); + while (n > 0) { + memcpy(truncated + n, "...", 4); + if (pdf_text_width(font, size, truncated) <= max_w) + break; + n--; + while (n > 0 && ((unsigned char)truncated[n] & 0xC0) == 0x80) + n--; + } + text = truncated; + } + pdf_text(p, x, baseline, font, size, rgb, text); + free(truncated); +} + static int parse_num(const char **sp, double *out) { const char *s = *sp; diff --git a/src/pdf.h b/src/pdf.h index c6f95ac..9ed4595 100644 --- a/src/pdf.h +++ b/src/pdf.h @@ -20,6 +20,12 @@ void pdf_text(struct pdf *p, double x, double baseline, const char *font, double size, const char *rgb, const char *utf8); double pdf_text_width(const char *font, double size, const char *utf8); double pdf_font_ascent(const char *font, double size); +/* Draws utf8 at x/baseline, shrinking the size (down to min_size) to fit + max_w; if it still does not fit, truncates at a whole UTF-8 character so + the text ends with "...". */ +void pdf_text_fit(struct pdf *p, double x, double baseline, const char *font, + double size, double min_size, double max_w, const char *rgb, + const char *utf8); void pdf_path(struct pdf *p, const char *path, double x, double y, const char *rgb); unsigned char *pdf_finish(struct pdf *p, size_t *len); diff --git a/src/util.c b/src/util.c index 9543875..7882642 100644 --- a/src/util.c +++ b/src/util.c @@ -276,6 +276,19 @@ char *util_str_trim(char *s) return s; } +int util_hex_color_valid(const char *s) +{ + if (!s || s[0] != '#' || strlen(s) != 7) + return 0; + for (int i = 1; i < 7; i++) { + char c = s[i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || + (c >= 'A' && c <= 'F'))) + return 0; + } + return 1; +} + static int is_leap(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; diff --git a/src/util.h b/src/util.h index d854f76..a94cf16 100644 --- a/src/util.h +++ b/src/util.h @@ -32,6 +32,7 @@ int util_const_eq(const void *a, const void *b, size_t n); int64_t util_now(void); void util_iso8601(int64_t t, char *buf, size_t n); char *util_str_trim(char *s); +int util_hex_color_valid(const char *s); int util_parse_iso_date(const char *s); /* base64 (standard alphabet, padding required) */ -- cgit v1.3