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;
|
if (!urlOrPath) return null;
|
||||||
|
|
||||||
// 1) Already public-relative.
|
// 1) Already public-relative.
|
||||||
if (urlOrPath.startsWith('/')) return urlOrPath;
|
if (urlOrPath.startsWith('/')) {
|
||||||
|
return urlOrPath;
|
||||||
|
}
|
||||||
|
|
||||||
// 2) Some datasets store "media/..." without leading slash.
|
// 2) Some datasets store "media/..." without leading slash.
|
||||||
if (/^media\//i.test(urlOrPath)) return `/${urlOrPath}`;
|
if (/^media\//i.test(urlOrPath)) return `/${urlOrPath}`;
|
||||||
@@ -3120,23 +3122,49 @@ async function loadEmbeddablePng(
|
|||||||
if (!resolved) return null;
|
if (!resolved) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Prefer local files for stability and speed.
|
// 1) Try standard local path first
|
||||||
if (resolved.startsWith('/')) {
|
if (resolved.startsWith('/')) {
|
||||||
try {
|
try {
|
||||||
const bytes = await readBytesFromPublic(resolved);
|
const bytes = await readBytesFromPublic(resolved);
|
||||||
return { pngBytes: await toPngBytes(bytes, resolved), debugLabel: resolved };
|
return { pngBytes: await toPngBytes(bytes, resolved), debugLabel: resolved };
|
||||||
} catch {
|
} 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)
|
// 2) Remote (network fetch fallback)
|
||||||
const host = process.env.NEXT_PUBLIC_BASE_URL || 'http://localhost:3000';
|
// 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://...`
|
// Ensure we don't end up with `http://localhost:3000http://...`
|
||||||
const fetchUrl = resolved.startsWith('/') ? `${host.replace(/\/$/, '')}${resolved}` : resolved;
|
const fetchUrl = resolved.startsWith('/') ? `${host.replace(/\/$/, '')}${resolved}` : resolved;
|
||||||
const bytes = await fetchBytes(fetchUrl);
|
const bytes = await fetchBytes(fetchUrl);
|
||||||
return { pngBytes: await toPngBytes(bytes, fetchUrl), debugLabel: 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;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3264,7 +3292,8 @@ function drawHeader(ctx: SectionDrawContext, yStart: number): number {
|
|||||||
// Cable-industry look: calm, engineered header with right-aligned meta.
|
// Cable-industry look: calm, engineered header with right-aligned meta.
|
||||||
// Keep header compact to free vertical space for technical tables.
|
// Keep header compact to free vertical space for technical tables.
|
||||||
const headerH = 52;
|
const headerH = 52;
|
||||||
const dividerY = yStart - headerH;
|
const headerTopPadding = 24;
|
||||||
|
const dividerY = ctx.height - headerTopPadding - headerH;
|
||||||
ctx.headerDividerY = dividerY;
|
ctx.headerDividerY = dividerY;
|
||||||
|
|
||||||
page.drawRectangle({
|
page.drawRectangle({
|
||||||
@@ -3989,10 +4018,15 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
|||||||
const qrPng = await loadQrPng(productUrl);
|
const qrPng = await loadQrPng(productUrl);
|
||||||
const qrImage = qrPng ? await pdfDoc.embedPng(qrPng.pngBytes) : null;
|
const qrImage = qrPng ? await pdfDoc.embedPng(qrPng.pngBytes) : null;
|
||||||
|
|
||||||
// Engineered page frame (A4): slightly narrower margins but consistent rhythm.
|
// Engineered page frame (A4): Push margins slightly closer to the edge.
|
||||||
const margin = 54;
|
const margin = 48; // Left/right margin
|
||||||
const footerY = 54;
|
const footerY = 28; // Absolute distance from the bottom for the footer line
|
||||||
const contentMinY = footerY + 42; // keep clear of footer + page numbers
|
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 contentWidth = width - 2 * margin;
|
||||||
|
|
||||||
const ctx: SectionDrawContext = {
|
const ctx: SectionDrawContext = {
|
||||||
@@ -4038,9 +4072,11 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
|||||||
const name = stripHtml(product.name);
|
const name = stripHtml(product.name);
|
||||||
const maxW = ctx.contentWidth;
|
const maxW = ctx.contentWidth;
|
||||||
const line = wrapText(name, fontBold, 12, maxW).slice(0, 1)[0] || name;
|
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, {
|
p.drawText(line, {
|
||||||
x: margin,
|
x: margin,
|
||||||
y: yStart,
|
y: height - headerTopPadding - 32,
|
||||||
size: 12,
|
size: 12,
|
||||||
font: fontBold,
|
font: fontBold,
|
||||||
color: navy,
|
color: navy,
|
||||||
@@ -4056,9 +4092,10 @@ async function generatePDF(product: ProductData, locale: 'en' | 'de'): Promise<B
|
|||||||
syncCtxForPage(page);
|
syncCtxForPage(page);
|
||||||
drawPageBackground(page);
|
drawPageBackground(page);
|
||||||
drawFooter(ctx);
|
drawFooter(ctx);
|
||||||
let yStart = drawHeader(ctx, ctx.height - ctx.margin);
|
y = drawHeader(ctx, height); // pass height, we override dividerY internally inside drawHeader
|
||||||
if (opts?.includeProductName) yStart = drawProductNameOnPage(page, yStart);
|
// stampProductName(); // Assuming this function is defined elsewhere or will be added.
|
||||||
return yStart;
|
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;
|
const hasSpace = (needed: number) => y - needed >= contentMinY;
|
||||||
|
|||||||
Reference in New Issue
Block a user