fix(pdf): push pdf header and footer closer to page edge and force payload image extraction fallback
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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