import isUrl from 'is-url'; import * as fontkit from 'fontkit'; import { PDFFont } from '@react-pdf/pdfkit'; // @ts-expect-error ts being silly const STANDARD_FONTS = [ 'Courier', 'Courier-Bold', 'Courier-Oblique', 'Courier-BoldOblique', 'Helvetica', 'Helvetica-Bold', 'Helvetica-Oblique', 'Helvetica-BoldOblique', 'Times-Roman', 'Times-Bold', 'Times-Italic', 'Times-BoldItalic', ]; class StandardFont { name; src; fullName; familyName; subfamilyName; postscriptName; copyright; version; underlinePosition; underlineThickness; italicAngle; bbox; 'OS/2'; hhea; numGlyphs; characterSet; availableFeatures; type; constructor(src) { this.name = src; this.fullName = src; this.familyName = src; this.subfamilyName = src; this.type = 'STANDARD'; this.postscriptName = src; this.availableFeatures = []; this.copyright = ''; this.version = 1; this.underlinePosition = -100; this.underlineThickness = 50; this.italicAngle = 0; this.bbox = {}; this['OS/2'] = {}; this.hhea = {}; this.numGlyphs = 0; this.characterSet = []; this.src = PDFFont.open(null, src); } encode(str) { return this.src.encode(str); } layout(str) { const [encoded, positions] = this.encode(str); const glyphs = encoded.map((g, i) => { const glyph = this.getGlyph(parseInt(g, 16)); glyph.advanceWidth = positions[i].advanceWidth; return glyph; }); const advanceWidth = positions.reduce((acc, p) => acc + p.advanceWidth, 0); return { positions, stringIndices: positions.map((_, i) => i), glyphs, script: 'latin', language: 'dflt', direction: 'ltr', features: {}, advanceWidth, advanceHeight: 0, bbox: undefined, }; } glyphForCodePoint(codePoint) { const glyph = this.getGlyph(codePoint); glyph.advanceWidth = 400; return glyph; } getGlyph(id) { return { id, codePoints: [id], isLigature: false, name: this.src.font.characterToGlyph(id), _font: this.src, // @ts-expect-error assign proper value advanceWidth: undefined, }; } hasGlyphForCodePoint(codePoint) { return this.src.font.characterToGlyph(codePoint) !== '.notdef'; } // Based on empirical observation get ascent() { return 900; } // Based on empirical observation get capHeight() { switch (this.name) { case 'Times-Roman': case 'Times-Bold': case 'Times-Italic': case 'Times-BoldItalic': return 650; case 'Courier': case 'Courier-Bold': case 'Courier-Oblique': case 'Courier-BoldOblique': return 550; default: return 690; } } // Based on empirical observation get xHeight() { switch (this.name) { case 'Times-Roman': case 'Times-Bold': case 'Times-Italic': case 'Times-BoldItalic': return 440; case 'Courier': case 'Courier-Bold': case 'Courier-Oblique': case 'Courier-BoldOblique': return 390; default: return 490; } } // Based on empirical observation get descent() { switch (this.name) { case 'Times-Roman': case 'Times-Bold': case 'Times-Italic': case 'Times-BoldItalic': return -220; case 'Courier': case 'Courier-Bold': case 'Courier-Oblique': case 'Courier-BoldOblique': return -230; default: return -200; } } get lineGap() { return 0; } get unitsPerEm() { return 1000; } stringsForGlyph() { throw new Error('Method not implemented.'); } glyphsForString() { throw new Error('Method not implemented.'); } widthOfGlyph() { throw new Error('Method not implemented.'); } getAvailableFeatures() { throw new Error('Method not implemented.'); } createSubset() { throw new Error('Method not implemented.'); } getVariation() { throw new Error('Method not implemented.'); } getFont() { throw new Error('Method not implemented.'); } getName() { throw new Error('Method not implemented.'); } setDefaultLanguage() { throw new Error('Method not implemented.'); } } const fetchFont = async (src, options) => { const response = await fetch(src, options); const data = await response.arrayBuffer(); return new Uint8Array(data); }; const isDataUrl = (dataUrl) => { const header = dataUrl.split(',')[0]; const hasDataPrefix = header.substring(0, 5) === 'data:'; const hasBase64Prefix = header.split(';')[1] === 'base64'; return hasDataPrefix && hasBase64Prefix; }; class FontSource { src; fontFamily; fontStyle; fontWeight; data; options; loadResultPromise; constructor(src, fontFamily, fontStyle, fontWeight, options) { this.src = src; this.fontFamily = fontFamily; this.fontStyle = fontStyle || 'normal'; this.fontWeight = fontWeight || 400; this.data = null; this.options = options || {}; this.loadResultPromise = null; } async _load() { const { postscriptName } = this.options; let data = null; if (STANDARD_FONTS.includes(this.src)) { data = new StandardFont(this.src); } else if (isDataUrl(this.src)) { const raw = this.src.split(',')[1]; const uint8Array = new Uint8Array(atob(raw) .split('') .map((c) => c.charCodeAt(0))); data = fontkit.create(uint8Array, postscriptName); } else if (isUrl(this.src)) { const { headers, body, method = 'GET' } = this.options; const buffer = await fetchFont(this.src, { method, body, headers }); data = fontkit.create(buffer, postscriptName); } else { data = await fontkit.open(this.src, postscriptName); } if (data && 'fonts' in data) { throw new Error('Font collection is not supported'); } this.data = data; } async load() { if (this.loadResultPromise === null) { this.loadResultPromise = this._load(); } return this.loadResultPromise; } } const FONT_WEIGHTS = { thin: 100, hairline: 100, ultralight: 200, extralight: 200, light: 300, normal: 400, medium: 500, semibold: 600, demibold: 600, bold: 700, ultrabold: 800, extrabold: 800, heavy: 900, black: 900, }; const resolveFontWeight = (value) => { return typeof value === 'string' ? FONT_WEIGHTS[value] : value; }; const sortByFontWeight = (a, b) => a.fontWeight - b.fontWeight; class FontFamily { family; sources; static create(family) { return new FontFamily(family); } constructor(family) { this.family = family; this.sources = []; } register({ src, fontWeight, fontStyle, ...options }) { const numericFontWeight = fontWeight ? resolveFontWeight(fontWeight) : undefined; this.sources.push(new FontSource(src, this.family, fontStyle, numericFontWeight, options)); } resolve(descriptor) { const { fontWeight = 400, fontStyle = 'normal' } = descriptor; const styleSources = this.sources.filter((s) => s.fontStyle === fontStyle); const exactFit = styleSources.find((s) => s.fontWeight === fontWeight); if (exactFit) return exactFit; // Weight resolution. https://developer.mozilla.org/en-US/docs/Web/CSS/font-weight#Fallback_weights let font = null; const numericFontWeight = resolveFontWeight(fontWeight); if (numericFontWeight >= 400 && numericFontWeight <= 500) { const leftOffset = styleSources.filter((s) => s.fontWeight <= numericFontWeight); const rightOffset = styleSources.filter((s) => s.fontWeight > 500); const fit = styleSources.filter((s) => s.fontWeight >= numericFontWeight && s.fontWeight < 500); font = fit[0] || leftOffset[leftOffset.length - 1] || rightOffset[0]; } const lt = styleSources .filter((s) => s.fontWeight < numericFontWeight) .sort(sortByFontWeight); const gt = styleSources .filter((s) => s.fontWeight > numericFontWeight) .sort(sortByFontWeight); if (numericFontWeight < 400) { font = lt[lt.length - 1] || gt[0]; } if (numericFontWeight > 500) { font = gt[0] || lt[lt.length - 1]; } if (!font) { throw new Error(`Could not resolve font for ${this.family}, fontWeight ${fontWeight}, fontStyle ${fontStyle}`); } return font; } } class FontStore { fontFamilies = {}; emojiSource = null; constructor() { this.register({ family: 'Helvetica', fonts: [ { src: 'Helvetica', fontStyle: 'normal', fontWeight: 400 }, { src: 'Helvetica-Bold', fontStyle: 'normal', fontWeight: 700 }, { src: 'Helvetica-Oblique', fontStyle: 'italic', fontWeight: 400 }, { src: 'Helvetica-BoldOblique', fontStyle: 'italic', fontWeight: 700 }, ], }); this.register({ family: 'Courier', fonts: [ { src: 'Courier', fontStyle: 'normal', fontWeight: 400 }, { src: 'Courier-Bold', fontStyle: 'normal', fontWeight: 700 }, { src: 'Courier-Oblique', fontStyle: 'italic', fontWeight: 400 }, { src: 'Courier-BoldOblique', fontStyle: 'italic', fontWeight: 700 }, ], }); this.register({ family: 'Times-Roman', fonts: [ { src: 'Times-Roman', fontStyle: 'normal', fontWeight: 400 }, { src: 'Times-Bold', fontStyle: 'normal', fontWeight: 700 }, { src: 'Times-Italic', fontStyle: 'italic', fontWeight: 400 }, { src: 'Times-BoldItalic', fontStyle: 'italic', fontWeight: 700 }, ], }); // For backwards compatibility this.register({ family: 'Helvetica-Bold', src: 'Helvetica-Bold', }); this.register({ family: 'Helvetica-Oblique', src: 'Helvetica-Oblique', }); this.register({ family: 'Helvetica-BoldOblique', src: 'Helvetica-BoldOblique', }); this.register({ family: 'Courier-Bold', src: 'Courier-Bold', }); this.register({ family: 'Courier-Oblique', src: 'Courier-Oblique', }); this.register({ family: 'Courier-BoldOblique', src: 'Courier-BoldOblique', }); this.register({ family: 'Times-Bold', src: 'Times-Bold', }); this.register({ family: 'Times-Italic', src: 'Times-Italic', }); this.register({ family: 'Times-BoldItalic', src: 'Times-BoldItalic', }); // Load default fonts this.load({ fontFamily: 'Helvetica', fontStyle: 'normal', fontWeight: 400, }); this.load({ fontFamily: 'Helvetica', fontStyle: 'normal', fontWeight: 700, }); this.load({ fontFamily: 'Helvetica', fontStyle: 'italic', fontWeight: 400, }); this.load({ fontFamily: 'Helvetica', fontStyle: 'italic', fontWeight: 700, }); } hyphenationCallback = null; register = (data) => { const { family } = data; if (!this.fontFamilies[family]) { this.fontFamilies[family] = FontFamily.create(family); } // Bulk loading if ('fonts' in data) { for (let i = 0; i < data.fonts.length; i += 1) { const { src, fontStyle, fontWeight, ...options } = data.fonts[i]; this.fontFamilies[family].register({ src, fontStyle, fontWeight, ...options, }); } } else { const { src, fontStyle, fontWeight, ...options } = data; this.fontFamilies[family].register({ src, fontStyle, fontWeight, ...options, }); } }; registerEmojiSource = (emojiSource) => { this.emojiSource = emojiSource; }; registerHyphenationCallback = (callback) => { this.hyphenationCallback = callback; }; getFont = (descriptor) => { const { fontFamily } = descriptor; if (!this.fontFamilies[fontFamily]) { throw new Error(`Font family not registered: ${fontFamily}. Please register it calling Font.register() method.`); } return this.fontFamilies[fontFamily].resolve(descriptor); }; load = async (descriptor) => { const font = this.getFont(descriptor); if (font) await font.load(); }; reset = () => { const keys = Object.keys(this.fontFamilies); for (let i = 0; i < keys.length; i += 1) { const key = keys[i]; for (let j = 0; j < this.fontFamilies[key].sources.length; j++) { const fontSource = this.fontFamilies[key].sources[j]; fontSource.data = null; } } }; clear = () => { this.fontFamilies = {}; }; getRegisteredFonts = () => this.fontFamilies; getEmojiSource = () => this.emojiSource; getHyphenationCallback = () => this.hyphenationCallback; getRegisteredFontFamilies = () => Object.keys(this.fontFamilies); } export { FontStore as default };