Fix header overlap in invoice PDF and Bongbetic wordmark on light theme
react-pdf wraps text once at the first flex measure; when yoga then shrank the vendor column the cached lines ran under the INVOICE meta column. The header now pins column widths (299 + 24 + 192 pt), sizes the logo from its own aspect (stacked above the text when it would leave under 160 pt), uses absolute per-Text line heights, and turns hyphenation off. Also: repeating table header with an orphan-header guard, wrap=false rows, Page n of N footer, GSTIN printed only when present, one Text per address line, HTML twin header grid. The light-theme header showed the white-text wordmark; the ternary was inverted. Adds vitest + pdfjs-dist harness that fails on the old document (address overlaps Invoice No.) and passes on the new one.
This commit is contained in:
@@ -0,0 +1,62 @@
|
||||
export interface ImageSize {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
function base64ToBytes(b64: string): Uint8Array {
|
||||
const bin = atob(b64);
|
||||
const out = new Uint8Array(bin.length);
|
||||
for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
|
||||
return out;
|
||||
}
|
||||
|
||||
const u16 = (b: Uint8Array, i: number) => (b[i] << 8) | b[i + 1];
|
||||
const u32 = (b: Uint8Array, i: number) => ((b[i] << 24) | (b[i + 1] << 16) | (b[i + 2] << 8) | b[i + 3]) >>> 0;
|
||||
|
||||
function pngSize(b: Uint8Array): ImageSize | null {
|
||||
const sig = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
|
||||
if (b.length < 24 || sig.some((v, i) => b[i] !== v)) return null;
|
||||
return { width: u32(b, 16), height: u32(b, 20) };
|
||||
}
|
||||
|
||||
function jpegSize(b: Uint8Array): ImageSize | null {
|
||||
if (b.length < 4 || b[0] !== 0xff || b[1] !== 0xd8) return null;
|
||||
let i = 2;
|
||||
while (i + 3 < b.length) {
|
||||
if (b[i] !== 0xff) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
const marker = b[i + 1];
|
||||
if (marker === 0xff) {
|
||||
i++; // fill byte
|
||||
continue;
|
||||
}
|
||||
// Standalone markers carry no length.
|
||||
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd8)) {
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
const isSof =
|
||||
marker >= 0xc0 && marker <= 0xcf && marker !== 0xc4 && marker !== 0xc8 && marker !== 0xcc;
|
||||
if (isSof) {
|
||||
if (i + 8 >= b.length) return null;
|
||||
return { height: u16(b, i + 5), width: u16(b, i + 7) };
|
||||
}
|
||||
i += 2 + u16(b, i + 2);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Pixel size of a PNG or JPEG data URI, or null when it is another format or malformed. */
|
||||
export function imageSizeFromDataUri(uri: string): ImageSize | null {
|
||||
const comma = uri.indexOf(",");
|
||||
if (!uri.startsWith("data:") || comma < 0) return null;
|
||||
try {
|
||||
const bytes = base64ToBytes(uri.slice(comma + 1));
|
||||
const size = pngSize(bytes) ?? jpegSize(bytes);
|
||||
return size && size.width > 0 && size.height > 0 ? size : null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { pdf } from "@react-pdf/renderer";
|
||||
import { save } from "@tauri-apps/plugin-dialog";
|
||||
import { registerPdfFonts } from "../pdf/fonts";
|
||||
import { InvoiceDocument, type InvoicePdfProps } from "../pdf/InvoiceDocument";
|
||||
import { api } from "./api";
|
||||
import type { BankAccount, Invoice, Settings } from "./types";
|
||||
@@ -64,6 +65,7 @@ export async function buildPdfProps(
|
||||
|
||||
/** Render props straight to a PDF blob. */
|
||||
export async function renderPdfBlob(props: InvoicePdfProps): Promise<Blob> {
|
||||
registerPdfFonts();
|
||||
return pdf(<InvoiceDocument {...props} />).toBlob();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user