many updates :-)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
/**
|
||||
* Fetches product images for invoice line items and returns them as
|
||||
* `data:` URLs ready to embed in the PDF.
|
||||
*
|
||||
* Uses a simple in-process LRU-ish Map keyed by URL. Images are typically
|
||||
* served from Shopify's CDN so re-fetching is cheap, but caching avoids
|
||||
* hammering the network when regenerating an invoice multiple times.
|
||||
*/
|
||||
const MAX_BYTES = 2 * 1024 * 1024; // 2 MB cap per image
|
||||
const CACHE_MAX_ENTRIES = 200;
|
||||
|
||||
const cache = new Map<string, string>(); // url -> data URL
|
||||
|
||||
function rememberInCache(url: string, dataUrl: string) {
|
||||
if (cache.size >= CACHE_MAX_ENTRIES) {
|
||||
// Drop the oldest entry (Map preserves insertion order).
|
||||
const oldest = cache.keys().next().value;
|
||||
if (oldest) cache.delete(oldest);
|
||||
}
|
||||
cache.set(url, dataUrl);
|
||||
}
|
||||
|
||||
function guessContentType(url: string, headerCt: string | null): string {
|
||||
if (headerCt && headerCt.startsWith("image/")) return headerCt;
|
||||
const lower = url.toLowerCase();
|
||||
if (lower.includes(".jpg") || lower.includes(".jpeg")) return "image/jpeg";
|
||||
if (lower.includes(".webp")) return "image/webp";
|
||||
if (lower.includes(".gif")) return "image/gif";
|
||||
return "image/png";
|
||||
}
|
||||
|
||||
export async function fetchProductImageDataUrl(url: string): Promise<string | undefined> {
|
||||
if (!url) return undefined;
|
||||
const hit = cache.get(url);
|
||||
if (hit) return hit;
|
||||
|
||||
// Request a small Shopify CDN variant when possible to keep the PDF lean.
|
||||
// Shopify image URLs accept a `width=` query param; fall back to the original URL.
|
||||
const requestUrl = url.includes("cdn.shopify.com") && !/[?&](width|height|crop)=/.test(url)
|
||||
? `${url}${url.includes("?") ? "&" : "?"}width=128`
|
||||
: url;
|
||||
|
||||
let res: Response;
|
||||
try {
|
||||
res = await fetch(requestUrl);
|
||||
} catch (err) {
|
||||
console.warn(`Product image fetch failed for ${url}:`, err);
|
||||
return undefined;
|
||||
}
|
||||
if (!res.ok) {
|
||||
console.warn(`Product image HTTP ${res.status} for ${url}`);
|
||||
return undefined;
|
||||
}
|
||||
const buf = await res.arrayBuffer();
|
||||
if (buf.byteLength === 0 || buf.byteLength > MAX_BYTES) return undefined;
|
||||
|
||||
const contentType = guessContentType(url, res.headers.get("content-type"));
|
||||
// @react-pdf supports png/jpeg natively; webp/gif are unreliable. Skip those.
|
||||
if (contentType !== "image/png" && contentType !== "image/jpeg") return undefined;
|
||||
|
||||
const b64 = Buffer.from(buf).toString("base64");
|
||||
const dataUrl = `data:${contentType};base64,${b64}`;
|
||||
rememberInCache(url, dataUrl);
|
||||
return dataUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves images for every line in parallel, mutating `imageDataUrl` in place.
|
||||
* Failures are swallowed (the row simply renders without an icon).
|
||||
*/
|
||||
export async function attachLineItemImages(
|
||||
lines: { imageUrl?: string; imageDataUrl?: string }[],
|
||||
): Promise<void> {
|
||||
await Promise.all(
|
||||
lines.map(async (line) => {
|
||||
if (!line.imageUrl) return;
|
||||
const dataUrl = await fetchProductImageDataUrl(line.imageUrl);
|
||||
if (dataUrl) line.imageDataUrl = dataUrl;
|
||||
}),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user