Skip to content

Instantly share code, notes, and snippets.

@bettysteger
Last active March 10, 2025 12:30
Show Gist options
  • Save bettysteger/499e5fc603df772858ba26825a4c5169 to your computer and use it in GitHub Desktop.
Save bettysteger/499e5fc603df772858ba26825a4c5169 to your computer and use it in GitHub Desktop.
jsPDF-with-emojis.js
// PDF Configuration
const PDF_CONFIG = {
unit: 'px',
hotfixes: ['px_scaling'],
putOnlyUsedFonts: true,
compress: true
};
const PDF_MARGIN = 80;
// Emoji handling utilities
const processEmoji = {
// Regex for matching emojis and common symbols
// Includes:
// - Basic symbols (⚠️, ℹ️, etc): \u2000-\u2BFF
// - Emojis: \u{1F300}-\u{1F9FF}
// - Variation Selectors (for emoji styling): \uFE00-\uFE0F
// - Zero Width Joiner (for compound emojis): \u200D
regex: /[\u2000-\u2BFF][\uFE00-\uFE0F]?|[\u{1F300}-\u{1F9FF}][\uFE00-\uFE0F]?|[\u{1F300}-\u{1F9FF}]\u200D[\u{1F300}-\u{1F9FF}][\uFE00-\uFE0F]?/gu,
// Create a canvas with emoji
createCanvas(emoji, parentElement) {
const computedStyle = window.getComputedStyle(parentElement);
const fontSize = parseInt(computedStyle.fontSize);
const emojiSize = Math.round(fontSize * 1.2); // 20% larger than text
const size = emojiSize * 2; // Double size for better quality
const canvas = document.createElement('canvas');
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext('2d');
ctx.font = `${size}px Arial`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(emoji, size/2, size/2);
return { canvas, emojiSize };
},
// Create an image element from canvas
createImageElement(canvas, emojiSize) {
const img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.style.width = `${emojiSize}px`;
img.style.height = `${emojiSize}px`;
img.style.verticalAlign = 'middle';
img.style.display = 'inline-block';
img.style.margin = '0 1px';
return img;
},
// Process a text node and replace emojis with images
processTextNode(node) {
const text = node.textContent;
const matches = text.match(this.regex);
if (!matches) return;
// Create document fragment for new content
const fragment = document.createDocumentFragment();
let lastIndex = 0;
matches.forEach((emoji, index) => {
// Add text before emoji
const textBefore = text.slice(lastIndex, text.indexOf(emoji, lastIndex));
if (textBefore) {
fragment.appendChild(document.createTextNode(textBefore));
}
// Add emoji image
const { canvas, emojiSize } = this.createCanvas(emoji, node.parentElement);
const img = this.createImageElement(canvas, emojiSize);
fragment.appendChild(img);
lastIndex = text.indexOf(emoji, lastIndex) + emoji.length;
});
// Add remaining text
if (lastIndex < text.length) {
fragment.appendChild(document.createTextNode(text.slice(lastIndex)));
}
node.parentNode.replaceChild(fragment, node);
}
};
const pdf = new jsPDF(PDF_CONFIG);
const element = document.body; // document.querySelector('.export-pdf');
const width = pdf.internal.pageSize.getWidth();
element.style.width = width + 'px';
element.style.fontFamily = 'Helvetica';
// Process all emojis in element
element.querySelectorAll('*').forEach(el => {
if (el.childNodes?.length) {
Array.from(el.childNodes)
.filter(node => node.nodeType === 3) // Text nodes only
.forEach(node => processEmoji.processTextNode(node));
}
});
// Generate PDF
pdf.html(element, {
callback: function (pdf) {
pdf.save('some-title.pdf');
},
autoPaging: 'text',
html2canvas: {
useCORS: true,
// fix image compression
// @see https://github.com/parallax/jsPDF/issues/3178#issuecomment-1384411475
onclone: () => {
const targetRect = element.getBoundingClientRect();
element.querySelectorAll('img').forEach((img) => {
const rect = img.getBoundingClientRect();
try {
pdf.addImage(img, 'JPEG', targetRect.x - rect.x, targetRect.y - rect.y, rect.width, rect.height);
} catch (e) {
console.error(`PDF image error: ${e}`);
}
img.remove();
});
}
},
margin: PDF_MARGIN,
width: width - PDF_MARGIN * 2,
windowWidth: width,
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment