UNPKG

pdf-lib

Version:

Create and modify PDF files with JavaScript

226 lines (187 loc) 7.13 kB
import { Font, Fontkit, Glyph } from 'src/types/fontkit'; import { createCmap } from 'src/core/embedders/CMap'; import { deriveFontFlags } from 'src/core/embedders/FontFlags'; import PDFHexString from 'src/core/objects/PDFHexString'; import PDFRef from 'src/core/objects/PDFRef'; import PDFString from 'src/core/objects/PDFString'; import PDFContext from 'src/core/PDFContext'; import { addRandomSuffix, byAscendingId, Cache, sortedUniq, toHexStringOfMinLength, } from 'src/utils'; /** * A note of thanks to the developers of https://github.com/foliojs/pdfkit, as * this class borrows from: * https://github.com/devongovett/pdfkit/blob/e71edab0dd4657b5a767804ba86c94c58d01fbca/lib/image/jpeg.coffee */ class CustomFontEmbedder { static async for(fontkit: Fontkit, fontData: Uint8Array) { const font = await fontkit.create(fontData); return new CustomFontEmbedder(font, fontData); } readonly font: Font; readonly scale: number; readonly fontData: Uint8Array; readonly fontName: string; protected baseFontName: string; protected glyphCache: Cache<Glyph[]>; protected constructor(font: Font, fontData: Uint8Array) { this.font = font; this.scale = 1000 / this.font.unitsPerEm; this.fontData = fontData; this.fontName = this.font.postscriptName || 'Font'; this.baseFontName = ''; this.glyphCache = Cache.populatedBy(this.allGlyphsInFontSortedById); } /** * Encode the JavaScript string into this font. (JavaScript encodes strings in * Unicode, but embedded fonts use their own custom encodings) */ encodeText(text: string): PDFHexString { const { glyphs } = this.font.layout(text); const hexCodes = new Array(glyphs.length); for (let idx = 0, len = glyphs.length; idx < len; idx++) { hexCodes[idx] = toHexStringOfMinLength(glyphs[idx].id, 4); } return PDFHexString.of(hexCodes.join('')); } // The advanceWidth takes into account kerning automatically, so we don't // have to do that manually like we do for the standard fonts. widthOfTextAtSize(text: string, size: number): number { const { glyphs } = this.font.layout(text); let totalWidth = 0; for (let idx = 0, len = glyphs.length; idx < len; idx++) { totalWidth += glyphs[idx].advanceWidth * this.scale; } const scale = size / 1000; return totalWidth * scale; } heightOfFontAtSize(size: number): number { const { ascent, descent, bbox } = this.font; const yTop = (ascent || bbox.maxY) * this.scale; const yBottom = (descent || bbox.minY) * this.scale; return ((yTop - yBottom) / 1000) * size; } sizeOfFontAtHeight(height: number): number { const { ascent, descent, bbox } = this.font; const yTop = (ascent || bbox.maxY) * this.scale; const yBottom = (descent || bbox.minY) * this.scale; return (1000 * height) / (yTop - yBottom); } embedIntoContext(context: PDFContext, ref?: PDFRef): Promise<PDFRef> { this.baseFontName = addRandomSuffix(this.fontName); return this.embedFontDict(context, ref); } protected async embedFontDict( context: PDFContext, ref?: PDFRef, ): Promise<PDFRef> { const cidFontDictRef = await this.embedCIDFontDict(context); const unicodeCMapRef = this.embedUnicodeCmap(context); const fontDict = context.obj({ Type: 'Font', Subtype: 'Type0', BaseFont: this.baseFontName, Encoding: 'Identity-H', DescendantFonts: [cidFontDictRef], ToUnicode: unicodeCMapRef, }); if (ref) { context.assign(ref, fontDict); return ref; } else { return context.register(fontDict); } } protected isCFF(): boolean { return this.font.cff; } protected async embedCIDFontDict(context: PDFContext): Promise<PDFRef> { const fontDescriptorRef = await this.embedFontDescriptor(context); const cidFontDict = context.obj({ Type: 'Font', Subtype: this.isCFF() ? 'CIDFontType0' : 'CIDFontType2', BaseFont: this.baseFontName, CIDSystemInfo: { Registry: PDFString.of('Adobe'), Ordering: PDFString.of('Identity'), Supplement: 0, }, FontDescriptor: fontDescriptorRef, W: this.computeWidths(), }); return context.register(cidFontDict); } protected async embedFontDescriptor(context: PDFContext): Promise<PDFRef> { const fontStreamRef = await this.embedFontStream(context); const { scale } = this; const { italicAngle, ascent, descent, capHeight, xHeight } = this.font; const { minX, minY, maxX, maxY } = this.font.bbox; const fontDescriptor = context.obj({ Type: 'FontDescriptor', FontName: this.baseFontName, Flags: deriveFontFlags(this.font), FontBBox: [minX * scale, minY * scale, maxX * scale, maxY * scale], ItalicAngle: italicAngle, Ascent: ascent * scale, Descent: descent * scale, CapHeight: (capHeight || ascent) * scale, XHeight: (xHeight || 0) * scale, // Not sure how to compute/find this, nor is anybody else really: // https://stackoverflow.com/questions/35485179/stemv-value-of-the-truetype-font StemV: 0, [this.isCFF() ? 'FontFile3' : 'FontFile2']: fontStreamRef, }); return context.register(fontDescriptor); } protected async serializeFont(): Promise<Uint8Array> { return this.fontData; } protected async embedFontStream(context: PDFContext): Promise<PDFRef> { const fontStream = context.flateStream(await this.serializeFont(), { Subtype: this.isCFF() ? 'CIDFontType0C' : undefined, }); return context.register(fontStream); } protected embedUnicodeCmap(context: PDFContext): PDFRef { const cmap = createCmap(this.glyphCache.access(), this.glyphId.bind(this)); const cmapStream = context.flateStream(cmap); return context.register(cmapStream); } protected glyphId(glyph?: Glyph): number { return glyph ? glyph.id : -1; } protected computeWidths(): Array<number | number[]> { const glyphs = this.glyphCache.access(); const widths: Array<number | number[]> = []; let currSection: number[] = []; for (let idx = 0, len = glyphs.length; idx < len; idx++) { const currGlyph = glyphs[idx]; const prevGlyph = glyphs[idx - 1]; const currGlyphId = this.glyphId(currGlyph); const prevGlyphId = this.glyphId(prevGlyph); if (idx === 0) { widths.push(currGlyphId); } else if (currGlyphId - prevGlyphId !== 1) { widths.push(currSection); widths.push(currGlyphId); currSection = []; } currSection.push(currGlyph.advanceWidth * this.scale); } widths.push(currSection); return widths; } private allGlyphsInFontSortedById = (): Glyph[] => { const glyphs: Glyph[] = new Array(this.font.characterSet.length); for (let idx = 0, len = glyphs.length; idx < len; idx++) { const codePoint = this.font.characterSet[idx]; glyphs[idx] = this.font.glyphForCodePoint(codePoint); } return sortedUniq(glyphs.sort(byAscendingId), (g) => g.id); }; } export default CustomFontEmbedder;