@spexzee/react-pdfhook
Version:
React hook for generating PDF documents from components
290 lines (289 loc) • 15 kB
JavaScript
;
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const react_1 = require("react");
const jspdf_1 = __importDefault(require("jspdf"));
const html2canvas_1 = __importDefault(require("html2canvas"));
const usePdfGenerator = (options = {}) => {
const pdfRef = (0, react_1.useRef)(null);
// Enhanced image loader that handles both URLs and local paths
const loadImage = (source) => __awaiter(void 0, void 0, void 0, function* () {
// Convert local paths to absolute URLs
if (!source.startsWith('http') && !source.startsWith('data:')) {
// Remove leading slash if present (public folder is served at root)
const cleanPath = source.startsWith('/') ? source.substring(1) : source;
source = `${window.location.origin}/${cleanPath}`;
}
return new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = 'Anonymous';
// Add timestamp to bypass cache
const timestamp = new Date().getTime();
const urlWithCacheBust = source.includes('?')
? `${source}&_=${timestamp}`
: `${source}?_=${timestamp}`;
img.onload = () => {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/png'));
};
img.onerror = (err) => {
console.error('Image load error:', err);
if (urlWithCacheBust !== source) {
img.src = source;
}
else {
reject(new Error(`Failed to load image: ${source}. CORS might be blocked.`));
}
};
img.src = urlWithCacheBust;
// Handle cached images
if (img.complete) {
const canvas = document.createElement('canvas');
canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight;
const ctx = canvas.getContext('2d');
ctx === null || ctx === void 0 ? void 0 : ctx.drawImage(img, 0, 0);
resolve(canvas.toDataURL('image/png'));
}
});
});
const generatePdf = (0, react_1.useCallback)((content) => __awaiter(void 0, void 0, void 0, function* () {
if (!pdfRef.current) {
if (options.debug)
console.error('PDF ref is not attached to any element');
return;
}
try {
const { fileName = 'document.pdf', format = 'a4', orientation = 'p', margin = 0, scale = 2, pageBreak = true, debug = false, fixedWidth, imageQuality = 1, } = options;
// Calculate margins
const marginValue = typeof margin === 'number' ? margin : 0;
const marginTop = typeof margin === 'number' ? margin : margin.top || marginValue;
const marginRight = typeof margin === 'number' ? margin : margin.right || marginValue;
const marginBottom = typeof margin === 'number' ? margin : margin.bottom || marginValue;
const marginLeft = typeof margin === 'number' ? margin : margin.left || marginValue;
const pdf = new jspdf_1.default(orientation, 'mm', format);
const pdfPageWidth = pdf.internal.pageSize.getWidth();
const effectivePdfWidth = fixedWidth
? Math.min(fixedWidth, pdfPageWidth - marginLeft - marginRight)
: pdfPageWidth - marginLeft - marginRight;
const pdfHeight = pdf.internal.pageSize.getHeight() - marginTop - marginBottom;
let yPos = marginTop;
const elementsToProcess = [];
// Process content array or default to all children
if (content === null || content === void 0 ? void 0 : content.length) {
for (const item of content) {
if (item.type === 'image') {
elementsToProcess.push(item.selector);
}
else if (item.mapping) {
const matches = Array.from(document.querySelectorAll(item.selector));
elementsToProcess.push(...matches);
}
else {
const element = document.querySelector(item.selector);
if (element)
elementsToProcess.push(element);
}
}
}
else {
elementsToProcess.push(...Array.from(pdfRef.current.children));
}
for (const item of elementsToProcess) {
if (typeof item === 'string') {
// Handle image URL or local path
const imageItem = content === null || content === void 0 ? void 0 : content.find((c) => c.selector === item && c.type === 'image');
const { width: desiredWidth = effectivePdfWidth, height: desiredHeight, x = marginLeft, format = 'JPEG', maintainAspectRatio = true, } = (imageItem === null || imageItem === void 0 ? void 0 : imageItem.imageOptions) || {};
try {
const imageDataUrl = yield loadImage(item);
const img = new Image();
img.src = imageDataUrl;
yield new Promise((resolve) => {
img.onload = () => resolve();
if (img.complete)
resolve();
});
// Calculate dimensions
let finalWidth = desiredWidth;
let finalHeight = desiredHeight ||
(maintainAspectRatio
? (img.naturalHeight * finalWidth) / img.naturalWidth
: effectivePdfWidth);
if (maintainAspectRatio) {
const aspectRatio = img.naturalWidth / img.naturalHeight;
if (desiredWidth && !desiredHeight) {
finalHeight = desiredWidth / aspectRatio;
}
else if (!desiredWidth && desiredHeight) {
finalWidth = desiredHeight * aspectRatio;
}
else if (!desiredWidth && !desiredHeight) {
finalWidth = Math.min(effectivePdfWidth, img.naturalWidth);
finalHeight = finalWidth / aspectRatio;
}
}
if (typeof finalWidth !== 'number' ||
typeof finalHeight !== 'number') {
throw new Error('Invalid image dimensions');
}
// Add image to PDF
pdf.addImage(imageDataUrl, format === 'PNG' ? 'PNG' : 'JPEG', x, yPos, finalWidth, finalHeight);
yPos += finalHeight + 10;
if (pageBreak && yPos > pdfHeight) {
pdf.addPage(format, orientation);
yPos = marginTop;
}
}
catch (error) {
if (options.debug)
console.error('Error processing image:', item, error);
continue;
}
}
else {
// Handle HTML element
const element = item;
if (window.getComputedStyle(element).display === 'none')
continue;
// Handle page breaks before element
if (pageBreak &&
element.dataset.pdfBreak === 'before' &&
yPos > marginTop) {
pdf.addPage(format, orientation);
yPos = marginTop;
}
// Temporarily adjust element dimensions
const originalStyles = {
width: element.style.width,
minWidth: element.style.minWidth,
overflow: element.style.overflow,
};
// Convert fixedWidth from mm to px (1mm ≈ 3.78px)
const htmlWidth = fixedWidth ? fixedWidth * 3.78 : undefined;
if (htmlWidth) {
element.style.width = `${htmlWidth}px`;
element.style.minWidth = `${htmlWidth}px`;
element.style.overflow = 'hidden';
}
// Configure html2canvas options
const html2canvasOptions = {
scale,
useCORS: true,
logging: debug,
windowWidth: htmlWidth || element.scrollWidth,
windowHeight: element.scrollHeight,
width: htmlWidth || element.scrollWidth,
height: element.scrollHeight,
scrollX: 0,
scrollY: 0,
x: 0,
y: 0,
backgroundColor: '#ffffff',
imageTimeout: 15000,
allowTaint: false,
ignoreElements: (_el) => false,
foreignObjectRendering: false,
onclone: (doc) => {
doc.querySelectorAll('.pdf-only').forEach((el) => {
el.style.display = 'block';
});
doc.querySelectorAll('.screen-only').forEach((el) => {
el.style.display = 'none';
});
},
};
const canvas = yield (0, html2canvas_1.default)(element, html2canvasOptions);
// Restore original styles
if (htmlWidth) {
element.style.width = originalStyles.width;
element.style.minWidth = originalStyles.minWidth;
element.style.overflow = originalStyles.overflow;
}
const imageData = canvas.toDataURL('image/png', imageQuality);
const imgWidth = effectivePdfWidth;
const imgHeight = (canvas.height * imgWidth) / canvas.width;
// Handle multi-page content
if (imgHeight > pdfHeight) {
let remainingHeight = imgHeight;
let currentImageHeight = 0;
while (remainingHeight > 0) {
const availableHeight = pdfHeight - yPos;
const heightToAdd = Math.min(availableHeight, remainingHeight);
pdf.addImage({
imageData,
format: 'PNG',
x: marginLeft,
y: yPos,
width: imgWidth,
height: imgHeight,
compression: 'FAST',
rotation: 0,
alias: undefined,
// @ts-ignore - jsPDF types don't include these but they work
sy: currentImageHeight,
sWidth: canvas.width,
sHeight: (heightToAdd / imgHeight) * canvas.height,
});
remainingHeight -= heightToAdd;
currentImageHeight += (heightToAdd / imgHeight) * canvas.height;
if (remainingHeight > 0) {
pdf.addPage(format, orientation);
yPos = marginTop;
}
else {
yPos += heightToAdd;
}
}
}
else {
if (pageBreak && yPos + imgHeight > pdfHeight) {
pdf.addPage(format, orientation);
yPos = marginTop;
}
pdf.addImage({
imageData,
format: 'PNG',
x: marginLeft,
y: yPos,
width: imgWidth,
height: imgHeight,
});
yPos += imgHeight;
}
// Handle page breaks after element
if (pageBreak && element.dataset.pdfBreak === 'after') {
pdf.addPage(format, orientation);
yPos = marginTop;
}
// Add spacing between elements
yPos += 5;
}
}
pdf.save(fileName);
const canvases = document.querySelectorAll('canvas[data-pdf-gen]');
canvases.forEach((canvas) => canvas.remove());
}
catch (error) {
if (options.debug)
console.error('Error generating PDF:', error);
throw error;
}
}), [options]);
return { generatePdf, pdfRef };
};
exports.default = usePdfGenerator;