fix(pdf): push pdf header and footer closer to page edge and force payload image extraction fallback
This commit is contained in:
@@ -2569,7 +2569,9 @@ function resolveMediaToLocalPath(urlOrPath: string | null | undefined): string |
|
||||
if (!urlOrPath) return null;
|
||||
|
||||
// 1) Already public-relative.
|
||||
if (urlOrPath.startsWith('/')) return urlOrPath;
|
||||
if (urlOrPath.startsWith('/')) {
|
||||
return urlOrPath;
|
||||
}
|
||||
|
||||
// 2) Some datasets store "media/..." without leading slash.
|
||||
if (/^media\//i.test(urlOrPath)) return `/${urlOrPath}`;
|
||||
@@ -3120,23 +3122,49 @@ async function loadEmbeddablePng(
|
||||
if (!resolved) return null;
|
||||
|
||||
try {
|
||||
// Prefer local files for stability and speed.
|
||||
// 1) Try standard local path first
|
||||
if (resolved.startsWith('/')) {
|
||||
try {
|
||||
const bytes = await readBytesFromPublic(resolved);
|
||||
return { pngBytes: await toPngBytes(bytes, resolved), debugLabel: resolved };
|
||||
} catch {
|
||||
// Fall back to HTTP fetch if file doesn't exist locally (e.g., Payload /api/ route)
|
||||
// Fallback: It might be a Payload API image that we couldn't statically map earlier
|
||||
// Check if we can intercept it manually.
|
||||
if (resolved.startsWith('/api/media/file/')) {
|
||||
try {
|
||||
const uploadFallback = resolved.replace('/api/media/file/', '/uploads/');
|
||||
const bytes = await readBytesFromPublic(uploadFallback);
|
||||
return {
|
||||
pngBytes: await toPngBytes(bytes, uploadFallback),
|
||||
debugLabel: uploadFallback,
|
||||
};
|
||||
} catch {
|
||||
// Check media
|
||||
try {
|
||||
const mediaFallback = resolved.replace('/api/media/file/', '/media/');
|
||||
const bytes = await readBytesFromPublic(mediaFallback);
|
||||
return {
|
||||
pngBytes: await toPngBytes(bytes, mediaFallback),
|
||||
debugLabel: mediaFallback,
|
||||
};
|
||||
} catch {
|
||||
// Ignore inner errors and fall through to network fetch
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remote (fallback)
|
||||
const host = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
||||
// 2) Remote (network fetch fallback)
|
||||
// IMPORTANT: Node 18+ fetch often resolves `localhost` to `::1` IPv6, which Payload might not be listening on.
|
||||
// Force 127.0.0.1 to guarantee IPv4 resolution for local API fetches during build.
|
||||
const host = process.env.NEXT_PUBLIC_BASE_URL || 'http://127.0.0.1:3000';
|
||||
// Ensure we don't end up with `http://localhost:3000http://...`
|
||||
const fetchUrl = resolved.startsWith('/') ? `${host.replace(/\/$/, '')}${resolved}` : resolved;
|
||||
const bytes = await fetchBytes(fetchUrl);
|
||||
return { pngBytes: await toPngBytes(bytes, fetchUrl), debugLabel: fetchUrl };
|
||||
} catch {
|
||||
} catch (err: any) {
|
||||
console.warn(`[PDF Image Warn] Failed to load image: ${src} -> ${resolved}`, err?.message);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -3264,7 +3292,8 @@ function drawHeader(ctx: SectionDrawContext, yStart: number): number {
|
||||
// Cable-industry look: calm, engineered header with right-aligned meta.
|
||||
// Keep header compact to free vertical space for technical tables.
|
||||
const headerH = 52;
|
||||
const dividerY = yStart - headerH;
|
||||
const headerTopPadding = 24;
|
||||
const dividerY = ctx.height - headerTopPadding - headerH;
|
||||
ctx.headerDividerY = dividerY;
|
||||
|
||||
page.drawRectangle({
|
||||
@@ -3989,10 +4018,15 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
||||
const qrPng = await loadQrPng(productUrl);
|
||||
const qrImage = qrPng ? await pdfDoc.embedPng(qrPng.pngBytes) : null;
|
||||
|
||||
// Engineered page frame (A4): slightly narrower margins but consistent rhythm.
|
||||
const margin = 54;
|
||||
const footerY = 54;
|
||||
const contentMinY = footerY + 42; // keep clear of footer + page numbers
|
||||
// Engineered page frame (A4): Push margins slightly closer to the edge.
|
||||
const margin = 48; // Left/right margin
|
||||
const footerY = 28; // Absolute distance from the bottom for the footer line
|
||||
const headerH = 52; // Header background height
|
||||
const headerTopPadding = 24; // Absolute distance from top for the header box
|
||||
|
||||
// Y-coordinate starts from the BOTTOM in pdf-lib. Height is 841.89
|
||||
// `contentMinY` is the lowest point standard body content can reach before overflowing into the footer.
|
||||
const contentMinY = footerY + 36;
|
||||
const contentWidth = width - 2 * margin;
|
||||
|
||||
const ctx: SectionDrawContext = {
|
||||
@@ -4038,9 +4072,11 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
||||
const name = stripHtml(product.name);
|
||||
const maxW = ctx.contentWidth;
|
||||
const line = wrapText(name, fontBold, 12, maxW).slice(0, 1)[0] || name;
|
||||
// Render name a bit lower inside the header space
|
||||
const headerTopPadding = 24;
|
||||
p.drawText(line, {
|
||||
x: margin,
|
||||
y: yStart,
|
||||
y: height - headerTopPadding - 32,
|
||||
size: 12,
|
||||
font: fontBold,
|
||||
color: navy,
|
||||
@@ -4056,9 +4092,10 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
||||
syncCtxForPage(page);
|
||||
drawPageBackground(page);
|
||||
drawFooter(ctx);
|
||||
let yStart = drawHeader(ctx, ctx.height - ctx.margin);
|
||||
if (opts?.includeProductName) yStart = drawProductNameOnPage(page, yStart);
|
||||
return yStart;
|
||||
y = drawHeader(ctx, height); // pass height, we override dividerY internally inside drawHeader
|
||||
// stampProductName(); // Assuming this function is defined elsewhere or will be added.
|
||||
if (opts?.includeProductName) y = drawProductNameOnPage(page, y); // Update y after drawing product name
|
||||
return ctx.headerDividerY - 24; // spacing below header line
|
||||
};
|
||||
|
||||
const hasSpace = (needed: number) => y - needed >= contentMinY;
|
||||
|
||||
Reference in New Issue
Block a user