update
This commit is contained in:
207
server.js
207
server.js
@@ -302,6 +302,7 @@ function normalizePdfExportPayload(input = {}) {
|
||||
const brandName = String(input.brandName || "JMK RB").trim();
|
||||
const brandTagline = String(input.brandTagline || "Live Event").trim();
|
||||
const footer = String(input.footer || "").trim();
|
||||
const logoDataUrl = String(input.logoDataUrl || "").trim();
|
||||
const theme = ["classic", "minimal", "motorsport"].includes(String(input.theme || "").toLowerCase())
|
||||
? String(input.theme).toLowerCase()
|
||||
: "classic";
|
||||
@@ -317,7 +318,7 @@ function normalizePdfExportPayload(input = {}) {
|
||||
}))
|
||||
: [];
|
||||
|
||||
return { title, subtitle, brandName, brandTagline, footer, theme, filename, sections };
|
||||
return { title, subtitle, brandName, brandTagline, footer, theme, logoDataUrl, filename, sections };
|
||||
}
|
||||
|
||||
function buildSimplePdf(payload) {
|
||||
@@ -325,10 +326,13 @@ function buildSimplePdf(payload) {
|
||||
const pageHeight = 842;
|
||||
const marginLeft = 42;
|
||||
const marginTop = 56;
|
||||
const marginBottom = 56;
|
||||
const lineHeight = 15;
|
||||
const maxChars = 86;
|
||||
const logo = parsePdfLogo(payload.logoDataUrl);
|
||||
const headerOffset = logo ? logo.displayHeight + 18 : 0;
|
||||
const lines = buildPdfLines(payload, maxChars);
|
||||
const linesPerPage = Math.max(10, Math.floor((pageHeight - marginTop * 2) / lineHeight));
|
||||
const linesPerPage = Math.max(10, Math.floor((pageHeight - marginTop - marginBottom - headerOffset) / lineHeight));
|
||||
const pages = [];
|
||||
for (let index = 0; index < lines.length; index += linesPerPage) {
|
||||
pages.push(lines.slice(index, index + linesPerPage));
|
||||
@@ -338,37 +342,168 @@ function buildSimplePdf(payload) {
|
||||
}
|
||||
|
||||
const objects = [];
|
||||
objects.push("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n");
|
||||
|
||||
const kids = [];
|
||||
const fontObjectId = 3 + pages.length * 2;
|
||||
const pageObjectIds = [];
|
||||
const contentObjectIds = [];
|
||||
let nextObjectId = 3;
|
||||
for (let index = 0; index < pages.length; index += 1) {
|
||||
const pageObjectId = 3 + index * 2;
|
||||
const contentObjectId = 4 + index * 2;
|
||||
kids.push(`${pageObjectId} 0 R`);
|
||||
const contentStream = buildPdfContentStream(pages[index], marginLeft, pageHeight - marginTop, lineHeight);
|
||||
objects.push(
|
||||
`${pageObjectId} 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${pageWidth} ${pageHeight}] /Resources << /Font << /F1 ${fontObjectId} 0 R >> >> /Contents ${contentObjectId} 0 R >>\nendobj\n`
|
||||
);
|
||||
objects.push(`${contentObjectId} 0 obj\n<< /Length ${Buffer.byteLength(contentStream, "utf8")} >>\nstream\n${contentStream}\nendstream\nendobj\n`);
|
||||
pageObjectIds.push(nextObjectId++);
|
||||
contentObjectIds.push(nextObjectId++);
|
||||
}
|
||||
objects.splice(1, 0, `2 0 obj\n<< /Type /Pages /Count ${pages.length} /Kids [${kids.join(" ")}] >>\nendobj\n`);
|
||||
objects.push(`${fontObjectId} 0 obj\n<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>\nendobj\n`);
|
||||
const fontObjectId = nextObjectId++;
|
||||
const imageObjectId = logo ? nextObjectId++ : null;
|
||||
|
||||
let pdf = "%PDF-1.4\n";
|
||||
objects.push(createPdfTextObject(1, "<< /Type /Catalog /Pages 2 0 R >>"));
|
||||
objects.push(createPdfTextObject(2, `<< /Type /Pages /Count ${pages.length} /Kids [${pageObjectIds.map((id) => `${id} 0 R`).join(" ")}] >>`));
|
||||
objects.push(createPdfTextObject(fontObjectId, "<< /Type /Font /Subtype /Type1 /BaseFont /Courier >>"));
|
||||
|
||||
if (logo && imageObjectId) {
|
||||
objects.push(
|
||||
createPdfStreamObject(
|
||||
imageObjectId,
|
||||
`<< /Type /XObject /Subtype /Image /Width ${logo.width} /Height ${logo.height} /ColorSpace /DeviceRGB /BitsPerComponent 8 /Filter /DCTDecode`,
|
||||
logo.data
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
for (let index = 0; index < pages.length; index += 1) {
|
||||
const resources = imageObjectId
|
||||
? `<< /Font << /F1 ${fontObjectId} 0 R >> /XObject << /Im1 ${imageObjectId} 0 R >> >>`
|
||||
: `<< /Font << /F1 ${fontObjectId} 0 R >> >>`;
|
||||
const contentStream = Buffer.from(
|
||||
buildPdfContentStream(
|
||||
pages[index],
|
||||
marginLeft,
|
||||
pageHeight - marginTop - headerOffset,
|
||||
lineHeight,
|
||||
logo
|
||||
? {
|
||||
x: marginLeft,
|
||||
y: pageHeight - marginTop - logo.displayHeight,
|
||||
width: logo.displayWidth,
|
||||
height: logo.displayHeight,
|
||||
}
|
||||
: null
|
||||
),
|
||||
"utf8"
|
||||
);
|
||||
objects.push(
|
||||
createPdfTextObject(
|
||||
pageObjectIds[index],
|
||||
`<< /Type /Page /Parent 2 0 R /MediaBox [0 0 ${pageWidth} ${pageHeight}] /Resources ${resources} /Contents ${contentObjectIds[index]} 0 R >>`
|
||||
)
|
||||
);
|
||||
objects.push(createPdfStreamObject(contentObjectIds[index], "<<", contentStream));
|
||||
}
|
||||
|
||||
return assemblePdf(objects);
|
||||
}
|
||||
|
||||
function createPdfTextObject(id, body) {
|
||||
return {
|
||||
id,
|
||||
buffer: Buffer.from(`${id} 0 obj\n${body}\nendobj\n`, "binary"),
|
||||
};
|
||||
}
|
||||
|
||||
function createPdfStreamObject(id, dictionaryPrefix, streamBuffer) {
|
||||
return {
|
||||
id,
|
||||
buffer: Buffer.concat([
|
||||
Buffer.from(`${id} 0 obj\n${dictionaryPrefix} /Length ${streamBuffer.length} >>\nstream\n`, "binary"),
|
||||
streamBuffer,
|
||||
Buffer.from("\nendstream\nendobj\n", "binary"),
|
||||
]),
|
||||
};
|
||||
}
|
||||
|
||||
function assemblePdf(objects) {
|
||||
const ordered = [...objects].sort((left, right) => left.id - right.id);
|
||||
const chunks = [Buffer.from("%PDF-1.4\n", "binary")];
|
||||
const offsets = [0];
|
||||
objects.forEach((object) => {
|
||||
offsets.push(Buffer.byteLength(pdf, "utf8"));
|
||||
pdf += object;
|
||||
let position = chunks[0].length;
|
||||
ordered.forEach((object) => {
|
||||
offsets[object.id] = position;
|
||||
chunks.push(object.buffer);
|
||||
position += object.buffer.length;
|
||||
});
|
||||
const xrefOffset = Buffer.byteLength(pdf, "utf8");
|
||||
pdf += `xref\n0 ${objects.length + 1}\n`;
|
||||
pdf += "0000000000 65535 f \n";
|
||||
offsets.slice(1).forEach((offset) => {
|
||||
pdf += `${String(offset).padStart(10, "0")} 00000 n \n`;
|
||||
});
|
||||
pdf += `trailer\n<< /Size ${objects.length + 1} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF`;
|
||||
return Buffer.from(pdf, "utf8");
|
||||
const xrefOffset = position;
|
||||
const size = ordered.length + 1;
|
||||
let xref = `xref\n0 ${size}\n0000000000 65535 f \n`;
|
||||
for (let id = 1; id < size; id += 1) {
|
||||
xref += `${String(offsets[id] || 0).padStart(10, "0")} 00000 n \n`;
|
||||
}
|
||||
chunks.push(Buffer.from(`${xref}trailer\n<< /Size ${size} /Root 1 0 R >>\nstartxref\n${xrefOffset}\n%%EOF`, "binary"));
|
||||
return Buffer.concat(chunks);
|
||||
}
|
||||
|
||||
function parsePdfLogo(dataUrl) {
|
||||
const match = /^data:image\/jpeg;base64,([A-Za-z0-9+/=\s]+)$/i.exec(String(dataUrl || "").trim());
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const data = Buffer.from(match[1].replace(/\s+/g, ""), "base64");
|
||||
const dimensions = getJpegDimensions(data);
|
||||
if (!dimensions) {
|
||||
return null;
|
||||
}
|
||||
const scale = Math.min(150 / dimensions.width, 52 / dimensions.height, 1);
|
||||
return {
|
||||
data,
|
||||
width: dimensions.width,
|
||||
height: dimensions.height,
|
||||
displayWidth: Math.max(1, Math.round(dimensions.width * scale)),
|
||||
displayHeight: Math.max(1, Math.round(dimensions.height * scale)),
|
||||
};
|
||||
}
|
||||
|
||||
function getJpegDimensions(buffer) {
|
||||
if (!Buffer.isBuffer(buffer) || buffer.length < 4 || buffer[0] !== 0xff || buffer[1] !== 0xd8) {
|
||||
return null;
|
||||
}
|
||||
let offset = 2;
|
||||
while (offset + 1 < buffer.length) {
|
||||
while (offset < buffer.length && buffer[offset] !== 0xff) {
|
||||
offset += 1;
|
||||
}
|
||||
while (offset < buffer.length && buffer[offset] === 0xff) {
|
||||
offset += 1;
|
||||
}
|
||||
if (offset >= buffer.length) {
|
||||
break;
|
||||
}
|
||||
const marker = buffer[offset];
|
||||
offset += 1;
|
||||
if (marker === 0xd9 || marker === 0xda) {
|
||||
break;
|
||||
}
|
||||
if (marker === 0x01 || (marker >= 0xd0 && marker <= 0xd7)) {
|
||||
continue;
|
||||
}
|
||||
if (offset + 1 >= buffer.length) {
|
||||
break;
|
||||
}
|
||||
const length = buffer.readUInt16BE(offset);
|
||||
if (length < 2 || offset + length > buffer.length) {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
(marker >= 0xc0 && marker <= 0xc3) ||
|
||||
(marker >= 0xc5 && marker <= 0xc7) ||
|
||||
(marker >= 0xc9 && marker <= 0xcb) ||
|
||||
(marker >= 0xcd && marker <= 0xcf)
|
||||
) {
|
||||
if (length >= 7) {
|
||||
return {
|
||||
height: buffer.readUInt16BE(offset + 3),
|
||||
width: buffer.readUInt16BE(offset + 5),
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
offset += length;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildPdfLines(payload, maxChars) {
|
||||
@@ -473,16 +608,24 @@ function wrapPdfText(text, maxChars) {
|
||||
return lines;
|
||||
}
|
||||
|
||||
function buildPdfContentStream(lines, x, y, lineHeight) {
|
||||
function buildPdfContentStream(lines, x, y, lineHeight, logoPlacement = null) {
|
||||
const escaped = lines.map((line) => escapePdfText(line));
|
||||
return [
|
||||
const commands = [];
|
||||
if (logoPlacement) {
|
||||
commands.push("q");
|
||||
commands.push(`${logoPlacement.width} 0 0 ${logoPlacement.height} ${logoPlacement.x} ${logoPlacement.y} cm`);
|
||||
commands.push("/Im1 Do");
|
||||
commands.push("Q");
|
||||
}
|
||||
commands.push(
|
||||
"BT",
|
||||
"/F1 11 Tf",
|
||||
`${lineHeight} TL`,
|
||||
`${x} ${y} Td`,
|
||||
...escaped.map((line) => `(${line}) Tj\nT*`),
|
||||
"ET",
|
||||
].join("\n");
|
||||
"ET"
|
||||
);
|
||||
return commands.join("\n");
|
||||
}
|
||||
|
||||
function escapePdfText(text) {
|
||||
|
||||
Reference in New Issue
Block a user