pdf-lib
Version:
Create and modify PDF files with JavaScript
250 lines (208 loc) • 7.73 kB
text/typescript
import { Font, Fontkit, Glyph, TypeFeatures } 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 {
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,
customName?: string,
fontFeatures?: TypeFeatures,
) {
const font = await fontkit.create(fontData);
return new CustomFontEmbedder(font, fontData, customName, fontFeatures);
}
readonly font: Font;
readonly scale: number;
readonly fontData: Uint8Array;
readonly fontName: string;
readonly customName: string | undefined;
readonly fontFeatures: TypeFeatures | undefined;
protected baseFontName: string;
protected glyphCache: Cache<Glyph[]>;
protected constructor(
font: Font,
fontData: Uint8Array,
customName?: string,
fontFeatures?: TypeFeatures,
) {
this.font = font;
this.scale = 1000 / this.font.unitsPerEm;
this.fontData = fontData;
this.fontName = this.font.postscriptName || 'Font';
this.customName = customName;
this.fontFeatures = fontFeatures;
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, this.fontFeatures);
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, this.fontFeatures);
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,
options: { descender?: boolean } = {},
): number {
const { descender = true } = options;
const { ascent, descent, bbox } = this.font;
const yTop = (ascent || bbox.maxY) * this.scale;
const yBottom = (descent || bbox.minY) * this.scale;
let height = yTop - yBottom;
if (!descender) height -= Math.abs(descent) || 0;
return (height / 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 =
this.customName || context.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',
CIDToGIDMap: 'Identity',
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(): (number | number[])[] {
const glyphs = this.glyphCache.access();
const widths: (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;