diff --git a/dev-playground/public/index.html b/dev-playground/public/index.html
index 66cecf303..9e77322e9 100644
--- a/dev-playground/public/index.html
+++ b/dev-playground/public/index.html
@@ -58,10 +58,10 @@
Playground does not make too much sense when horizontal resolution is below
var editor = ace.edit('editor');
setupEditor(editor);
- var names = ['basics', 'styles1', 'styles2', 'styles3', 'columns', 'tables', 'lists', 'margin', 'images', 'svgs'];
+ var names = ['basics', 'styles1', 'styles2', 'styles3', 'columns', 'tables', 'lists', 'margin', 'images', 'svgs', 'acroforms'];
var i = 0;
- ['basics', 'named-styles', 'inline-styling', 'style-overrides', 'columns', 'tables', 'lists', 'margins', 'images', 'svgs'].forEach(function (example) {
+ ['basics', 'named-styles', 'inline-styling', 'style-overrides', 'columns', 'tables', 'lists', 'margins', 'images', 'svgs', 'acroforms'].forEach(function (example) {
$scope.examples.push({
name: names[i++],
activate: function () {
diff --git a/dev-playground/public/samples/acroforms b/dev-playground/public/samples/acroforms
new file mode 100644
index 000000000..9abcca983
--- /dev/null
+++ b/dev-playground/public/samples/acroforms
@@ -0,0 +1,338 @@
+subsetFonts: false,
+content: [
+ {text: 'Acroforms', style: 'header'},
+ {
+ text: [
+ "It is recommended that you test your PDF form ",
+ "documents across all platforms and viewers that you wish to ",
+ "support. Refer to ",
+ "https://pdfkit.org/docs/forms or the PDF ",
+ "reference for form options and advanced form field use.\n\n"
+ ],
+ style: 'description'
+ },
+
+ {text: 'Components\n', style: 'subHeader'},
+ {
+ text: [
+ "Make sure you set the subsetFonts flag to false when using ",
+ "form fields with text.\n\n"
+ ],
+ style: {bold: true}
+ },
+ {text: 'Text field', style: 'formHeader'},
+ {
+ columns: [
+ [
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_placeholder',
+ options: {
+ value: 'Placeholder ...'
+ }
+
+ },
+ style: 'textFieldStyle',
+ height: 15,
+ italics: true,
+ },
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_date',
+ options: {
+ format: {
+ type: 'date',
+ } ,
+ align: 'center',
+ value: '10/12'
+ }
+
+ },
+ style: 'textFieldStyle',
+ height: 15,
+
+ },
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_multi',
+ options: {
+ multiline: true,
+ value: "multiline text form "
+ }
+ },
+ style: 'textFieldStyle',
+ height: 50,
+ },
+ ],
+ [
+
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_alignment',
+ options: {
+ align: 'right',
+ value: 'right alignment'
+ }
+
+ },
+ style: 'textFieldStyle',
+ height: 15,
+ },
+
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_color',
+ options: {
+ align: 'center',
+ required: true,
+ value: 'Ω©®℅¥123'
+ },
+ },
+ style: 'textFieldStyle',
+ height: 15,
+
+ },
+ ],
+ [
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_currency',
+ options: {
+ align: 'right',
+ format: {
+ type: 'number',
+ nDec: 2,
+ sepComma: true,
+ negStyle: 'ParensRed',
+ currency: '$',
+ currencyPrepend: true
+ },
+ value: '$123,346.99'
+ }
+ },
+ style: 'textFieldStyle',
+ height: 15,
+ },
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_backgroundcolor',
+ options: {
+ backgroundColor: 'yellow',
+ borderColor: 'green',
+ align: 'center',
+ value: 'background color'
+ }
+ },
+ style: 'textFieldStyle',
+ height: 15,
+ },
+ ],
+ ],
+
+ },
+ {
+ columns: [
+ [
+ {text: '\nList', style: 'formHeader'},
+ {
+ acroform: {
+ type: 'list',
+ id: 'list1',
+ options: {
+ select: ['', 'A', 'B', 'C'],
+ }
+
+ },
+ width: 100,
+ height: 60,
+
+ },
+ ],
+ [
+ {text: '\nCombobox', style: 'formHeader'},
+ {
+ acroform: {
+ type: 'combo',
+ id: 'combo1',
+ options: {
+ select: ['', 'A', 'B', 'C'],
+ defaultValue: ''
+ }
+
+ },
+ width: 100,
+ height: 20,
+
+ },
+ ],
+ [
+ {text: '\nCheckbox form', style: 'formHeader'},
+ {
+ acroform: {
+ type: 'checkbox',
+ id: 'checkbox1',
+ options: {
+ selected: false,
+ }
+ },
+ width: 20,
+ height: 20,
+
+ },
+ ],
+ [
+ {text: '\nRadio form', style: 'formHeader'},
+ {
+ acroform: {
+ type: 'radio',
+ id: 'radioChild1',
+ options: {
+ parentId: 'radioForm1',
+ selected: true,
+ },
+ },
+ width: 50,
+ height: 30,
+
+ },
+ {
+ acroform: {
+ type: 'radio',
+ id: 'radioChild2',
+ options: {
+ parentId: 'radioForm1',
+ },
+ },
+ width: 40,
+ height: 25,
+
+ },
+ {
+ acroform: {
+ type: 'radio',
+ id: 'radioChild3',
+ options: {
+ parentId: 'radioForm1',
+ },
+ },
+ width: 30,
+ height: 20,
+ },
+ ],
+ ]
+ },
+ {
+ text: '\nYou can also use forms inline with text\n\n',
+ style: 'subHeader'
+ },
+ {
+ text: [
+ "Check this box! ",
+ {
+ acroform: {
+ type: 'checkbox',
+ id: 'checkbox2',
+ options: {
+ selected: false
+ }
+ },
+ width: 15,
+ height: 15,
+
+ },
+ '\n\n'
+ ],
+ alignment: 'center'
+ },
+ {
+ text: [
+ "The weather was very ",
+ {
+ acroform: {
+ type: 'combo',
+ id: 'combo_story1',
+ options: {
+ select: ['', 'nice', 'bad'],
+ defaultValue: ''
+ }
+
+ },
+ width: 50,
+ height: 9.5,
+
+ },
+ " and the skies were ",
+ {
+ acroform: {
+ type: 'combo',
+ id: 'combo_story2',
+ options: {
+ select: ['', 'clear', 'cloudy'],
+ defaultValue: ''
+ }
+
+ },
+ width: 50,
+ height: 9.5,
+
+ },
+ ". A fox named ",
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_story1',
+ },
+ width: 50,
+ height: 9.5,
+ },
+ " and friends were playing a game at the park, when suddenly",
+ "... ",
+ {
+ text: 'finish the story',
+ style: {italics: true, bold: true}
+
+ },
+ ],
+ },
+ {
+ acroform: {
+ type: 'text',
+ id: 'text_story2',
+ options: {
+ multiline: true
+ }
+ },
+ width: '*',
+ height: 100,
+ },
+],
+styles: {
+ header: {
+ fontSize: 18,
+ bold: true,
+ margin: [0, 0, 0, 10],
+ lineHeight: 0.3,
+ },
+ description: {
+ fontSize: 13,
+ margin: [0, 10, 0, 5]
+ },
+ subHeader: { bold: true, fontSize: 13 },
+ formHeader: {
+ fontSize: 12,
+ color: 'black'
+ },
+ tableCell: {
+ margin: [0, 5, 0, 5]
+ },
+ textFieldStyle: {
+ margin: [0, 0, 5, 5]
+ }
+}
\ No newline at end of file
diff --git a/dev-playground/server.js b/dev-playground/server.js
index ff2fe12f9..10519ba48 100644
--- a/dev-playground/server.js
+++ b/dev-playground/server.js
@@ -35,6 +35,7 @@ app.post('/pdf', function (req, res) {
res.contentType('application/pdf');
res.send(binary);
}, function (error) {
+ console.log(error) //print in console
res.send('ERROR:' + error);
});
diff --git a/package.json b/package.json
index 4cd51f230..c5b7f9f48 100644
--- a/package.json
+++ b/package.json
@@ -60,7 +60,8 @@
"build:fonts": "shx mkdir -p build/fonts && shx mkdir -p build/fonts/Roboto && shx cp -r fonts/Roboto/*.* build/fonts/Roboto && brfs \"./src/browser-extensions/fonts/Roboto.js\" > build/fonts/Roboto.js",
"lint": "eslint \"./src/**/*.js\" \"./tests/**/*.js\" \"./examples/**/*.js\" \"./standard-fonts/**/*.js\" \"./fonts/**/*.js\"",
"mocha": "mocha --reporter spec \"./tests/**/*.spec.js\"",
- "playground": "node dev-playground/server.js"
+ "playground": "node dev-playground/server.js",
+ "playground:watch": "nodemon dev-playground/server.js"
},
"repository": {
"type": "git",
diff --git a/src/DocMeasure.js b/src/DocMeasure.js
index 7bb6653d7..ed72e1873 100644
--- a/src/DocMeasure.js
+++ b/src/DocMeasure.js
@@ -61,7 +61,10 @@ class DocMeasure {
return extendMargins(this.measureCanvas(node));
} else if (node.qr) {
return extendMargins(this.measureQr(node));
- } else {
+ } else if (node.acroform) {
+ return extendMargins(this.measureAcroForm(node));
+ }
+ else {
throw new Error(`Unrecognized document structure: ${stringifyNode(node)}`);
}
});
@@ -686,7 +689,24 @@ class DocMeasure {
measureQr(node) {
node = qrEncoder.measure(node);
- node._alignment = this.styleStack.getProperty('alignment');
+ node._styleStack = this.styleStack.getProperty('alignment');
+ return node;
+ }
+
+ measureAcroForm(node) {
+ node._minWidth = 10;
+ node._minHeight = 10;
+
+ let font = StyleContextStack.getStyleProperty(node, this.styleStack, 'font', 'Roboto');
+ let bold = StyleContextStack.getStyleProperty(node, this.styleStack, 'bold', false);
+ let italics = StyleContextStack.getStyleProperty(node, this.styleStack, 'italics', false);
+
+ node.font = font;
+ node.bold = bold;
+ node.italics = italics;
+
+ node.alignment = StyleContextStack.getStyleProperty(node, this.styleStack, 'alignment', 'left');
+
return node;
}
}
diff --git a/src/DocPreprocessor.js b/src/DocPreprocessor.js
index ce1a4cc80..d4971db82 100644
--- a/src/DocPreprocessor.js
+++ b/src/DocPreprocessor.js
@@ -32,7 +32,6 @@ class DocPreprocessor {
} else if ('text' in node) { // cast value in text property
node.text = convertValueToString(node.text);
}
-
if (node.columns) {
return this.preprocessColumns(node);
} else if (node.stack) {
@@ -57,6 +56,8 @@ class DocPreprocessor {
return this.preprocessQr(node);
} else if (node.pageReference || node.textReference) {
return this.preprocessText(node);
+ } else if (node.acroform) {
+ return this.preprocessAcroForm(node);
} else {
throw new Error(`Unrecognized document structure: ${stringifyNode(node)}`);
}
@@ -242,6 +243,10 @@ class DocPreprocessor {
return node;
}
+ preprocessAcroForm(node) {
+ return node;
+ }
+
preprocessQr(node) {
return node;
}
diff --git a/src/ElementWriter.js b/src/ElementWriter.js
index ec70cb219..b4c05cbb4 100644
--- a/src/ElementWriter.js
+++ b/src/ElementWriter.js
@@ -23,7 +23,7 @@ class ElementWriter extends EventEmitter {
let context = this.context();
let page = context.getCurrentPage();
let position = this.getCurrentPositionOnPage();
-
+
if (context.availableHeight < height || !page) {
return false;
}
@@ -42,7 +42,7 @@ class ElementWriter extends EventEmitter {
if (!dontUpdateContextPosition) {
context.moveDown(height);
}
-
+
return position;
}
@@ -196,6 +196,33 @@ class ElementWriter extends EventEmitter {
return position;
}
+ addAcroForm(node, index) {
+ let context = this.context();
+ let page = context.getCurrentPage();
+ let position = this.getCurrentPositionOnPage();
+
+ if (!page) {
+ return false;
+ }
+
+ if (node._x === undefined) {
+ node._x = node.x || 0;
+ }
+
+ addPageItem(page, {
+ type: 'acroform',
+ item: node
+ }, index);
+
+ node.x = context.x + node._x;
+ node.y = context.y;
+
+
+ context.moveDown(node.height || node._minHeight);
+
+ return position;
+ }
+
alignImage(image) {
let width = this.context().availableWidth;
let imageWidth = image._minWidth;
diff --git a/src/LayoutBuilder.js b/src/LayoutBuilder.js
index 982754cdf..651c240f1 100644
--- a/src/LayoutBuilder.js
+++ b/src/LayoutBuilder.js
@@ -77,7 +77,7 @@ class LayoutBuilder {
let nodeInfo = {};
[
'id', 'text', 'ul', 'ol', 'table', 'image', 'qr', 'canvas', 'svg', 'columns',
- 'headlineLevel', 'style', 'pageBreak', 'pageOrientation',
+ 'headlineLevel', 'style', 'pageBreak', 'pageOrientation', 'acroForm', 'type', 'options',
'width', 'height'
].forEach(key => {
if (node[key] !== undefined) {
@@ -439,6 +439,8 @@ class LayoutBuilder {
this.processCanvas(node);
} else if (node.qr) {
this.processQr(node);
+ } else if (node.acroform) {
+ this.processAcroForm(node);
} else if (!node._span) {
throw new Error(`Unrecognized document structure: ${stringifyNode(node)}`);
}
@@ -645,7 +647,7 @@ class LayoutBuilder {
processor.endTable(this.writer);
}
- // leafs (texts)
+ // leafs (texts, acroform)
processLeaf(node) {
let line = this.buildNextLine(node);
if (line && (node.tocItem || node.id)) {
@@ -724,7 +726,7 @@ class LayoutBuilder {
let inline = textNode._inlines.shift();
isForceContinue = false;
- if (!inline.noWrap && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
+ if (!inline.noWrap && inline.text && inline.text.length > 1 && inline.width > line.getAvailableWidth()) {
let widthPerChar = inline.width / inline.text.length;
let maxChars = Math.floor(line.getAvailableWidth() / widthPerChar);
if (maxChars < 1) {
@@ -774,6 +776,13 @@ class LayoutBuilder {
let position = this.writer.addQr(node);
node.positions.push(position);
}
+
+ processAcroForm (node) {
+ let availableWidth = this.writer.context().availableWidth;
+ let position = this.writer.addAcroForm(node);
+ node.positions.push(position);
+ node.availableWidth = availableWidth;
+ }
}
function decorateNode(node) {
diff --git a/src/Line.js b/src/Line.js
index 7ff24511f..293dc28b1 100644
--- a/src/Line.js
+++ b/src/Line.js
@@ -33,13 +33,7 @@ class Line {
* @returns {number}
*/
getHeight() {
- let max = 0;
-
- this.inlines.forEach(item => {
- max = Math.max(max, item.height || 0);
- });
-
- return max;
+ return Math.max(...this.inlines.map(item => item.height || 0));
}
/**
@@ -49,7 +43,7 @@ class Line {
let y = 0;
this.inlines.forEach(inline => {
- y = Math.max(y, inline.font.ascender / 1000 * inline.fontSize);
+ y = Math.max(y, inline.acroform ? inline.height : inline.font.ascender / 1000 * inline.fontSize);
});
return y;
diff --git a/src/PDFDocument.js b/src/PDFDocument.js
index a9683863e..8bcb2bd2d 100644
--- a/src/PDFDocument.js
+++ b/src/PDFDocument.js
@@ -1,4 +1,7 @@
+import ExtendedAcroFormMixin from './pdf-kit-extensions/ExtendedAcroFormMixin';
+import PDFEmbeddedFont from './pdf-kit-extensions/PDFEmbeddedFont';
import PDFKit from '@foliojs-fork/pdfkit';
+import { isStandardFont } from './pdf-kit-extensions/StandardFonts';
const typeName = (bold, italics) => {
let type = 'normal';
@@ -11,9 +14,9 @@ const typeName = (bold, italics) => {
}
return type;
};
-
+
class PDFDocument extends PDFKit {
- constructor(fonts = {}, images = {}, patterns = {}, options = {}, virtualfs = null) {
+ constructor(fonts = {}, images = {}, patterns = {}, options = {}, virtualfs = null, subsetFonts = true) {
super(options);
this.fonts = {};
@@ -39,9 +42,9 @@ class PDFDocument extends PDFKit {
}
}
-
this.images = images;
this.virtualfs = virtualfs;
+ this.subsetFonts = subsetFonts; //TODO maybe automatically set this flag
}
getFontType(bold, italics) {
@@ -75,7 +78,20 @@ class PDFDocument extends PDFKit {
def[0] = this.virtualfs.readFileSync(def[0]);
}
- this.fontCache[familyName][type] = this.font(...def)._font;
+ if (this.subsetFonts == false && !isStandardFont(def[0])) {
+ this._font = new PDFEmbeddedFont(
+ this,
+ def[0],
+ `F${++this._fontCount}`,
+ );
+
+ this._fontFamilies[def[0]] = this._font;
+ this._fontFamilies[this._font.name] = this._font;
+
+ this.fontCache[familyName][type] = this._font;
+ } else {
+ this.fontCache[familyName][type] = this.font(...def)._font;
+ }
}
return this.fontCache[familyName][type];
@@ -145,4 +161,10 @@ class PDFDocument extends PDFKit {
}
}
+export function mixin(methods) {
+ Object.assign(PDFDocument.prototype, methods);
+}
+
+mixin(ExtendedAcroFormMixin);
+
export default PDFDocument;
diff --git a/src/PageElementWriter.js b/src/PageElementWriter.js
index a9b2ff27b..756a50598 100644
--- a/src/PageElementWriter.js
+++ b/src/PageElementWriter.js
@@ -35,6 +35,10 @@ class PageElementWriter extends ElementWriter {
return this._fitOnPage(() => super.addQr(qr, index));
}
+ addAcroForm(node, index) {
+ return this._fitOnPage(() => super.addAcroForm(node, index));
+ }
+
addVector(vector, ignoreContextX, ignoreContextY, index) {
return super.addVector(vector, ignoreContextX, ignoreContextY, index);
}
diff --git a/src/Printer.js b/src/Printer.js
index 6aeca159d..ef3b35bc4 100644
--- a/src/Printer.js
+++ b/src/Printer.js
@@ -68,8 +68,8 @@ class PdfPrinter {
font: null
};
- this.pdfKitDoc = new PDFDocument(this.fontDescriptors, docDefinition.images, docDefinition.patterns, pdfOptions, this.virtualfs);
-
+ this.pdfKitDoc = new PDFDocument(this.fontDescriptors, docDefinition.images, docDefinition.patterns, pdfOptions, this.virtualfs, docDefinition.subsetFonts);
+
const builder = new LayoutBuilder(pageSize, normalizePageMargin(docDefinition.pageMargins), new SVGMeasure());
builder.registerTableLayouts(tableLayouts);
diff --git a/src/Renderer.js b/src/Renderer.js
index 19bf464a0..7713a9855 100644
--- a/src/Renderer.js
+++ b/src/Renderer.js
@@ -41,6 +41,7 @@ class Renderer {
constructor(pdfDocument, progressCallback) {
this.pdfDocument = pdfDocument;
this.progressCallback = progressCallback;
+ this.hasFormInit = false;
}
renderPages(pages) {
@@ -65,6 +66,7 @@ class Renderer {
let page = pages[i];
for (let ii = 0, il = page.items.length; ii < il; ii++) {
let item = page.items[ii];
+
switch (item.type) {
case 'vector':
this.renderVector(item.item);
@@ -78,6 +80,9 @@ class Renderer {
case 'svg':
this.renderSVG(item.item);
break;
+ case 'acroform':
+ this.renderAcroForm(item.item);
+ break;
case 'beginClip':
this.beginClip(item.item);
break;
@@ -144,48 +149,59 @@ class Renderer {
let inline = line.inlines[i];
let shiftToBaseline = lineHeight - ((inline.font.ascender / 1000) * inline.fontSize) - descent;
- if (inline._pageNodeRef) {
- preparePageNodeRefLine(inline._pageNodeRef, inline);
- }
-
- let options = {
- lineBreak: false,
- textWidth: inline.width,
- characterSpacing: inline.characterSpacing,
- wordCount: 1,
- link: inline.link
- };
-
- if (inline.linkToDestination) {
- options.goTo = inline.linkToDestination;
- }
-
- if (line.id && i === 0) {
- options.destination = line.id;
- }
-
- if (inline.fontFeatures) {
- options.features = inline.fontFeatures;
- }
-
- let opacity = isNumber(inline.opacity) ? inline.opacity : 1;
- this.pdfDocument.opacity(opacity);
- this.pdfDocument.fill(inline.color || 'black');
-
- this.pdfDocument._font = inline.font;
- this.pdfDocument.fontSize(inline.fontSize);
-
- let shiftedY = offsetText(y + shiftToBaseline, inline);
- this.pdfDocument.text(inline.text, x + inline.x, shiftedY, options);
+ if (inline.acroform) {
+ //TODO positioning issue
+ let shiftedY = y + (lineHeight - ((inline.font.ascender / 1000) * inline.height) - descent);
+ inline.y = shiftedY;
+ inline.x = x + inline.x;
- if (inline.linkToPage) {
- this.pdfDocument.ref({ Type: 'Action', S: 'GoTo', D: [inline.linkToPage, 0, 0] }).end();
- this.pdfDocument.annotate(x + inline.x, shiftedY, inline.width, inline.height, { Subtype: 'Link', Dest: [inline.linkToPage - 1, 'XYZ', null, null, null] });
+ this.renderAcroForm(inline);
+ } else {
+ if (inline._pageNodeRef) {
+ preparePageNodeRefLine(inline._pageNodeRef, inline);
+ }
+
+ let options = {
+ lineBreak: false,
+ textWidth: inline.width,
+ characterSpacing: inline.characterSpacing,
+ wordCount: 1,
+ link: inline.link
+ };
+
+ if (inline.linkToDestination) {
+ options.goTo = inline.linkToDestination;
+ }
+
+ if (line.id && i === 0) {
+ options.destination = line.id;
+ }
+
+ if (inline.fontFeatures) {
+ options.features = inline.fontFeatures;
+ }
+
+ let opacity = isNumber(inline.opacity) ? inline.opacity : 1;
+ this.pdfDocument.opacity(opacity);
+ this.pdfDocument.fill(inline.color || 'black');
+
+ this.pdfDocument._font = inline.font;
+ this.pdfDocument.fontSize(inline.fontSize);
+
+ let shiftedY = offsetText(y + shiftToBaseline, inline);
+ this.pdfDocument.text(inline.text, x + inline.x, shiftedY, options);
+
+ if (inline.linkToPage) {
+ this.pdfDocument.ref({ Type: 'Action', S: 'GoTo', D: [inline.linkToPage, 0, 0] }).end();
+ this.pdfDocument.annotate(x + inline.x, shiftedY, inline.width, inline.height, { Subtype: 'Link', Dest: [inline.linkToPage - 1, 'XYZ', null, null, null] });
+ }
}
+
}
// Decorations won't draw correctly for superscript
textDecorator.drawDecorations(line, x, y);
+
}
renderVector(vector) {
@@ -326,6 +342,84 @@ class Renderer {
SVGtoPDF(this.pdfDocument, svg.svg, svg.x, svg.y, options);
}
+
+ renderAcroForm(node) {
+ const { font, bold, italics } = node;
+ let { type, options } = node.acroform;
+
+ if (options == null) {
+ options = {};
+ }
+
+ const setFont = () => {
+ if (typeof font === "string") {
+ this.pdfDocument._font = this.pdfDocument.provideFont(font, bold, italics);
+ } else {
+ this.pdfDocument._font = font;
+ }
+ if (this.hasFormInit) {
+ this.pdfDocument.addFontToAcroFormDict();
+ }
+ };
+
+ if (this.hasFormInit == false) {
+ setFont();
+ this.pdfDocument.initForm();
+ this.hasFormInit = true;
+ }
+
+ const id = node.acroform.id;
+
+ if (id == null) {
+ throw new Error(`Acroform field ${id} requires an ID`);
+ }
+
+ let width = node.width || node.availableWidth || (!isNaN(node._calcWidth) && node._calcWidth) || node._minWidth;
+ if (node.width == '*') {
+ width = node.availableWidth;
+ }
+
+ if (width == null) {
+ throw new Error(`Form ${type} width is undefined`);
+ }
+
+ let resolvedType;
+
+ setFont();
+
+ switch (type) {
+ case "text":
+ case "formText":
+ resolvedType = "formText";
+ break;
+ case "button":
+ case "formPushButton":
+ resolvedType = "formPushButton";
+ break;
+ case "list":
+ case "formList":
+ resolvedType = "formList";
+ break;
+ case "combo":
+ case "formCombo":
+ resolvedType = "formCombo";
+ break;
+ case "checkbox":
+ case "formCheckbox":
+ resolvedType = "formCheckbox";
+ break;
+ case "radio":
+ case "formRadio":
+ case "formRadioButton":
+ resolvedType = "formRadioButton";
+ break;
+ default:
+ throw new Error(`Unrecognized acroform type: ${type}`);
+ }
+
+ this.pdfDocument[resolvedType](id, node.x, node.y, width, node.height, options);
+ }
+
beginClip(rect) {
this.pdfDocument.save();
this.pdfDocument.addContent(`${rect.x} ${rect.y} ${rect.width} ${rect.height} re`);
diff --git a/src/TextBreaker.js b/src/TextBreaker.js
index e43ae07a0..5fa322834 100644
--- a/src/TextBreaker.js
+++ b/src/TextBreaker.js
@@ -104,16 +104,23 @@ class TextBreaker {
let noWrap = StyleContextStack.getStyleProperty(item || {}, styleContextStack, 'noWrap', false);
if (isObject(item)) {
- if (item._textRef && item._textRef._textNodeRef.text) {
- item.text = item._textRef._textNodeRef.text;
+ if (item.text) {
+ if (item._textRef && item._textRef._textNodeRef.text) {
+ item.text = item._textRef._textNodeRef.text;
+ }
+ words = splitWords(item.text, noWrap);
+ style = StyleContextStack.copyStyle(item);
+ } else if (item.acroform) {
+ words = [item];
+ style = StyleContextStack.copyStyle(item);
}
- words = splitWords(item.text, noWrap);
- style = StyleContextStack.copyStyle(item);
+
} else {
words = splitWords(item, noWrap);
}
- if (lastWord && words.length) {
+ //TODO: handle if last or first is not text
+ if (lastWord && words.length && !lastWord.acroform) {
let firstWord = getFirstWord(words, noWrap);
let wrapWords = splitWords(lastWord + firstWord, false);
@@ -123,10 +130,16 @@ class TextBreaker {
}
for (let i2 = 0, l2 = words.length; i2 < l2; i2++) {
- let result = {
- text: words[i2].text
- };
+ let result = {};
+ if (words[0].acroform) {
+ result = words[0];
+ } else {
+ result = {
+ text: words[i2].text
+ };
+ }
+
if (words[i2].lineEnd) {
result.lineEnd = true;
}
@@ -138,7 +151,11 @@ class TextBreaker {
lastWord = null;
if (i + 1 < l) {
- lastWord = getLastWord(words, noWrap);
+ if (words[0].acroform) {
+ lastWord = words[0];
+ } else {
+ lastWord = getLastWord(words, noWrap);
+ }
}
}
diff --git a/src/TextInlines.js b/src/TextInlines.js
index a3aa2fecf..660a43916 100644
--- a/src/TextInlines.js
+++ b/src/TextInlines.js
@@ -121,7 +121,7 @@ class TextInlines {
item.link = StyleContextStack.getStyleProperty(item, styleContextStack, 'link', null);
item.linkToPage = StyleContextStack.getStyleProperty(item, styleContextStack, 'linkToPage', null);
item.linkToDestination = StyleContextStack.getStyleProperty(item, styleContextStack, 'linkToDestination', null);
- item.noWrap = StyleContextStack.getStyleProperty(item, styleContextStack, 'noWrap', null);
+ item.noWrap = item.acroform ? true: StyleContextStack.getStyleProperty(item, styleContextStack, 'noWrap', null);
item.opacity = StyleContextStack.getStyleProperty(item, styleContextStack, 'opacity', 1);
item.sup = StyleContextStack.getStyleProperty(item, styleContextStack, 'sup', false);
item.sub = StyleContextStack.getStyleProperty(item, styleContextStack, 'sub', false);
@@ -133,30 +133,38 @@ class TextInlines {
let lineHeight = StyleContextStack.getStyleProperty(item, styleContextStack, 'lineHeight', 1);
- item.width = this.widthOfText(item.text, item);
- item.height = item.font.lineHeight(item.fontSize) * lineHeight;
+ if (item.acroform) {
+ item.width = item.width || 25;
+ item.height = item.height || 15;
+ } else {
+ item.width = this.widthOfText(item.text, item);
+ item.height = item.font.lineHeight(item.fontSize) * lineHeight;
- if (!item.leadingCut) {
- item.leadingCut = 0;
- }
-
- let preserveLeadingSpaces = StyleContextStack.getStyleProperty(item, styleContextStack, 'preserveLeadingSpaces', false);
- if (!preserveLeadingSpaces) {
- let leadingSpaces = item.text.match(LEADING);
- if (leadingSpaces) {
- item.leadingCut += this.widthOfText(leadingSpaces[0], item);
+ if (!item.leadingCut) {
+ item.leadingCut = 0;
}
- }
-
- item.trailingCut = 0;
-
- let preserveTrailingSpaces = StyleContextStack.getStyleProperty(item, styleContextStack, 'preserveTrailingSpaces', false);
- if (!preserveTrailingSpaces) {
- let trailingSpaces = item.text.match(TRAILING);
- if (trailingSpaces) {
- item.trailingCut = this.widthOfText(trailingSpaces[0], item);
+
+ let preserveLeadingSpaces = StyleContextStack.getStyleProperty(item, styleContextStack, 'preserveLeadingSpaces', false);
+ if (!preserveLeadingSpaces) {
+ let leadingSpaces = item.text.match(LEADING);
+ if (leadingSpaces) {
+ item.leadingCut += this.widthOfText(leadingSpaces[0], item);
+ }
+ }
+
+ item.trailingCut = 0;
+
+ let preserveTrailingSpaces = StyleContextStack.getStyleProperty(item, styleContextStack, 'preserveTrailingSpaces', false);
+ if (!preserveTrailingSpaces) {
+ let trailingSpaces = item.text.match(TRAILING);
+ if (trailingSpaces) {
+ item.trailingCut = this.widthOfText(trailingSpaces[0], item);
+ }
}
}
+
+
+
}, this);
return array;
diff --git a/src/pdf-kit-extensions/ExtendedAcroFormMixin.js b/src/pdf-kit-extensions/ExtendedAcroFormMixin.js
new file mode 100644
index 000000000..11c47e10d
--- /dev/null
+++ b/src/pdf-kit-extensions/ExtendedAcroFormMixin.js
@@ -0,0 +1,224 @@
+/**
+ * Includes checkboxes and radio group forms
+ */
+const ExtendedAcroFormMixin = {
+
+ extendedFormAnnotation(name, type, x, y, w, h, options = {}, appearanceId) {
+ let resolvedType = type == 'radioChild' ? null : type;
+ let fieldDict = this._fieldDict(name, resolvedType, options);
+
+ //TODO adobe acrobat doesn't like 0 font size
+ Object.assign(fieldDict, {
+ DR: this.page.resources,
+ DA: new String(`/${this._font.id} ${options.fontSize || this._fontSize} Tf 0 g`),
+ });
+
+ if (fieldDict.fontSize) {
+ delete fieldDict.fontSize;
+ }
+
+ fieldDict.Subtype = 'Widget';
+
+ if (fieldDict.F === undefined) {
+ fieldDict.F = 4;
+ }
+
+ if ((type == 'checkbox' || type == 'radioChild') && appearanceId) {
+ this.createAppearances(x, y, w, h, fieldDict, type, appearanceId);
+ }
+
+ this.annotate(x, y, w, h, fieldDict);
+
+ let annotRef = this.page.annotations[this.page.annotations.length - 1];
+ return this._addToParent(annotRef);
+
+ },
+
+ createAppearances(x, y, w, h, options = {}, type, appearanceId) {
+ const dimentions = [x, y, w, h];
+
+ if (type == 'radioChild') { //radio field
+ delete options.T;
+ }
+
+ if (options.selected) {
+ options.AS = appearanceId;
+ options.V = appearanceId;
+
+ } else {
+ options.AS = "Off";
+ options.V = "Off";
+ }
+
+ delete options.selected;
+ delete options.fontSize;
+
+ let resolvedType;
+ switch (type) {
+ case 'radioChild':
+ resolvedType = 'RadioButton';
+ break;
+ case 'checkbox':
+ resolvedType = 'Checkbox';
+ break;
+ default:
+ break;
+ }
+
+ const CA = Appearance[resolvedType].getCA();
+ const AP = Appearance[resolvedType].createAppearance(this, appearanceId, dimentions);
+
+ Object.assign(options, {MK: {CA}, AP});
+
+ return options;
+ },
+
+ _createAppearanceField(dimentions, stream) {
+ const appr = this.ref({
+ Type: 'XObject',
+ Subtype: 'Form',
+ FormType: 1,
+ BBox: dimentions,
+ Resources: this.page.resources
+ });
+
+ appr.end(stream);
+
+ return appr;
+ },
+
+ _getParentRadioField(parentName) {
+ const ref = this._getParent(parentName);
+ let groupRef;
+
+ if (ref == null) {
+ groupRef = this.formField(parentName, {
+ FT: 'Btn',
+ Ff: 32768, //TODO
+ F: 4,
+ T: new String(parentName),
+ Kids: [],
+ });
+ } else {
+ groupRef = ref;
+ }
+
+ if (groupRef == null) {
+ throw new Error(`Unable to create parent: ${parentName}`);
+ }
+
+ return groupRef;
+ },
+
+ _getParent(parentName) {
+ return this._root.data.AcroForm.data.Fields.filter(ref => ref.data.T != null && ref.data.T == parentName)[0];
+ },
+
+ formRadioButton(name, x, y, w, h, options = {}) {
+ if (options.parentId == null) {
+ throw new Error(`Unable to find key 'parentId' for radio form field: ${name}`);
+ }
+
+ const parentName = options.parentId;
+
+ options.Parent = this._getParentRadioField(parentName);
+
+ if (options.selected) {
+ this._getParent(parentName).data.V = name;
+ }
+
+ delete options.parentId;
+
+ return this.extendedFormAnnotation(name, 'radioChild', x, y, w, h, options, name);
+ },
+
+ formCheckbox(name, x, y, w, h, options = {}) {
+ return this.extendedFormAnnotation(name, 'checkbox', x, y, w, h, options, "On");
+ },
+
+ formText(name, x, y, w, h, options = {}) {
+ return this.extendedFormAnnotation(name, 'text', x, y, w, h, options);
+ },
+
+ formPushButton(name, x, y, w, h, options = {}) {
+ return this.extendedFormAnnotation(name, 'pushButton', x, y, w, h, options);
+ },
+
+ formCombo(name, x, y, w, h, options = {}) {
+ return this.extendedFormAnnotation(name, 'combo', x, y, w, h, options);
+ },
+
+ formList(name, x, y, w, h, options = {}) {
+ return this.extendedFormAnnotation(name, 'list', x, y, w, h, options);
+ },
+
+ addFontToAcroFormDict() {
+ this._acroform.fonts[this._font.id] = this._font.ref();
+ }
+};
+
+//TODO doc definition to customise this
+const Appearance = {
+ Checkbox: {
+ createAppearance(pdfDocument, appearanceId, dimentions) {
+ const APStream = this.getTrueAPStream(pdfDocument);
+ return {
+ N: {
+ [appearanceId]: pdfDocument._createAppearanceField(dimentions, APStream),
+ "Off": pdfDocument._createAppearanceField(dimentions, APStream)
+ },
+ //R
+ D: {
+ [appearanceId]: pdfDocument._createAppearanceField(dimentions, APStream),
+ "Off": pdfDocument._createAppearanceField(dimentions, APStream)
+ }
+ };
+ },
+ getTrueAPStream(pdfDocument) {
+ return `
+q
+0 0 1 rg
+BT
+ /${pdfDocument._font.id} ${pdfDocument._fontSize} Tf
+ 0 0 Td
+ (${this.getCA()}) Tj
+ET
+Q`;
+ },
+ getCA() {
+ return new String(3);
+ }
+ },
+ RadioButton: {
+ createAppearance(pdfDocument, appearanceId, dimentions) {
+ const APStream = this.getTrueAPStream(pdfDocument);
+ return {
+ N: {
+ [appearanceId]: pdfDocument._createAppearanceField(dimentions, APStream),
+ "Off": pdfDocument._createAppearanceField(dimentions, APStream)
+ },
+ //R
+ D: {
+ [appearanceId]: pdfDocument._createAppearanceField(dimentions, APStream),
+ "Off": pdfDocument._createAppearanceField(dimentions, APStream)
+ }
+ };
+ },
+ getTrueAPStream(pdfDocument) {
+ return `
+q
+0 0 1 rg
+BT
+ /${pdfDocument._font.id} ${pdfDocument._fontSize} Tf
+ 0 0 Td
+ (${this.getCA()}) Tj
+ET
+Q`;
+ },
+ getCA() {
+ return new String(8);
+ }
+ }
+};
+
+export default ExtendedAcroFormMixin;
diff --git a/src/pdf-kit-extensions/PDFEmbeddedFont.js b/src/pdf-kit-extensions/PDFEmbeddedFont.js
new file mode 100644
index 000000000..ed5bfa5a7
--- /dev/null
+++ b/src/pdf-kit-extensions/PDFEmbeddedFont.js
@@ -0,0 +1,321 @@
+var fs = require('fs');
+var fontkit = require('@foliojs-fork/fontkit');
+
+class PDFEmbeddedFont {
+ constructor(document, src, id) {
+ this.document = document;
+ this.src = src;
+ this.dictionary = document.dictionary;
+
+ if (typeof src === 'string') {
+ this.font = fontkit.create(fs.readFileSync(src));
+ } else if (Buffer.isBuffer(src)) {
+ this.font = fontkit.create(src);
+ } else {
+ this.font = src;
+ }
+
+ this.id = id;
+ this.name = this.font.postscriptName;
+ this.scale = 1000 / this.font.unitsPerEm;
+
+ this.unicode = {};
+ this.widths = [this.font.getGlyph(0).advanceWidth];
+
+ this.font.characterSet.forEach(codePoint => {
+ if (this.font.hasGlyphForCodePoint(codePoint)) {
+ const glyph = this.font.glyphForCodePoint(codePoint);
+ this.unicode[glyph.id] = ([codePoint]);
+ this.widths[glyph.id] = (glyph.advanceWidth * this.scale);
+ }
+ });
+
+ this.ascender = this.font.ascent * this.scale;
+ this.descender = this.font.descent * this.scale;
+ this.xHeight = this.font.xHeight * this.scale;
+ this.capHeight = this.font.capHeight * this.scale;
+ this.lineGap = this.font.lineGap * this.scale;
+ this.bbox = this.font.bbox;
+
+ if (document.options.fontLayoutCache !== false) {
+ this.layoutCache = Object.create(null);
+ }
+ }
+
+ layoutRun(text, features) {
+ /**
+ * TODO:
+ * font layout returns a single unicode point, instead of its substituions, line 25
+ * liga false but can be overriden
+ */
+ const run = this.font.layout(text, {liga: false,...features}); // Normalize position values
+
+ for (let i = 0; i < run.positions.length; i++) {
+ const position = run.positions[i];
+
+ for (let key in position) {
+ position[key] *= this.scale;
+ }
+
+ position.advanceWidth = run.glyphs[i].advanceWidth * this.scale;
+ }
+
+ return run;
+ }
+
+ layoutCached(text) {
+ if (!this.layoutCache) {
+ return this.layoutRun(text);
+ }
+
+ let cached = this.layoutCache[text];
+
+ if (cached) {
+ return cached;
+ }
+
+ const run = this.layoutRun(text);
+ this.layoutCache[text] = run;
+ return run;
+ }
+
+ layout(text, features, onlyWidth) {
+ // Skip the cache if any user defined features are applied
+ if (features) {
+ return this.layoutRun(text, features);
+ }
+
+ let glyphs = onlyWidth ? null : [];
+ let positions = onlyWidth ? null : [];
+ let advanceWidth = 0; // Split the string by words to increase cache efficiency.
+ // For this purpose, spaces and tabs are a good enough delimeter.
+
+ let last = 0;
+ let index = 0;
+
+ while (index <= text.length) {
+ var needle;
+
+ if (index === text.length && last < index || (needle = text.charAt(index), [' ', '\t'].includes(needle))) {
+ const run = this.layoutCached(text.slice(last, ++index));
+
+ if (!onlyWidth) {
+ glyphs = glyphs.concat(run.glyphs);
+ positions = positions.concat(run.positions);
+ }
+
+ advanceWidth += run.advanceWidth;
+ last = index;
+ } else {
+ index++;
+ }
+ }
+
+ return {
+ glyphs,
+ positions,
+ advanceWidth
+ };
+ }
+
+ ref() {
+ return this.dictionary != null ? this.dictionary : this.dictionary = this.document.ref();
+ }
+
+ finalize() {
+ if (this.embedded || this.dictionary == null) {
+ return;
+ }
+
+ this.embed();
+ return this.embedded = true;
+ }
+
+ encode(text, features) {
+ const {
+ glyphs,
+ positions
+ } = this.layout(text, features);
+
+ const res = [];
+
+ for (let i = 0; i < glyphs.length; i++) {
+ const glyph = glyphs[i];
+ const gid = glyph.id;
+
+ res.push(`0000${gid.toString(16)}`.slice(-4));
+ }
+
+ return [res, positions];
+ }
+
+ widthOfString(string, size, features) {
+ const width = this.layout(string, features, true).advanceWidth;
+ const scale = size / 1000;
+ return width * scale;
+ }
+
+ embed() {
+ const isCFF = this.font.cff != null;
+ const fontFile = this.document.ref();
+
+ if (isCFF) {
+ fontFile.data.Subtype = 'CIDFontType0C';
+ }
+
+ fs.createReadStream(this.src)
+ .on("data", function (data) {
+ fontFile.write(data);
+ })
+ .on("end", function () {
+ fontFile.end();
+ });
+
+ const familyClass = ((this.font['OS/2'] != null ? this.font['OS/2'].sFamilyClass : undefined) || 0) >> 8;
+ let flags = 0;
+
+ if (this.font.post.isFixedPitch) {
+ flags |= 1 << 0;
+ }
+
+ if (1 <= familyClass && familyClass <= 7) {
+ flags |= 1 << 1;
+ }
+
+ flags |= 1 << 2; // assume the font uses non-latin characters
+
+ if (familyClass === 10) {
+ flags |= 1 << 3;
+ }
+
+ if (this.font.head.macStyle.italic) {
+ flags |= 1 << 6;
+ } // generate a tag (6 uppercase letters. 17 is the char code offset from '0' to 'A'. 73 will map to 'Z')
+
+ const name = this.font.postscriptName;
+
+ const {
+ bbox
+ } = this.font;
+ const descriptor = this.document.ref({
+ Type: 'FontDescriptor',
+ FontName: name,
+ Flags: flags,
+ FontBBox: [bbox.minX * this.scale, bbox.minY * this.scale, bbox.maxX * this.scale, bbox.maxY * this.scale],
+ ItalicAngle: this.font.italicAngle,
+ Ascent: this.ascender,
+ Descent: this.descender,
+ CapHeight: (this.font.capHeight || this.font.ascent) * this.scale,
+ XHeight: (this.font.xHeight || 0) * this.scale,
+ StemV: 0
+ }); // not sure how to calculate this
+
+ if (isCFF) {
+ descriptor.data.FontFile3 = fontFile;
+ } else {
+ descriptor.data.FontFile2 = fontFile;
+ }
+
+ descriptor.end();
+ const descendantFontData = {
+ Type: 'Font',
+ Subtype: 'CIDFontType0',
+ BaseFont: name,
+ CIDSystemInfo: {
+ Registry: new String('Adobe'),
+ Ordering: new String('Identity'),
+ Supplement: 0
+ },
+ FontDescriptor: descriptor,
+ W: [0, this.widths]
+ };
+
+ if (!isCFF) {
+ descendantFontData.Subtype = 'CIDFontType2';
+ descendantFontData.CIDToGIDMap = 'Identity';
+ }
+
+ const descendantFont = this.document.ref(descendantFontData);
+ descendantFont.end();
+ this.dictionary.data = {
+ Type: 'Font',
+ Subtype: 'Type0',
+ BaseFont: name,
+ Encoding: 'Identity-H',
+ DescendantFonts: [descendantFont],
+ ToUnicode: this.toUnicodeCmap()
+ };
+
+ return this.dictionary.end();
+ }
+
+ lineHeight(size, includeGap) {
+ if (includeGap == null) {
+ includeGap = false;
+ }
+
+ const gap = includeGap ? this.lineGap : 0;
+ return (this.ascender + gap - this.descender) / 1000 * size;
+ }
+
+
+ toUnicodeCmap() {
+ const cmap = this.document.ref();
+ const characters = () => {
+ let entries = "";
+
+ const toHex = function (num) {
+ return `0000${num.toString(16)}`.slice(-4);
+ };
+
+ for (let key of Object.keys(this.unicode)) {
+ const codePoints = this.unicode[key];
+ const encoded = []; // encode codePoints to utf16
+
+ for (let value of codePoints) {
+ if (value > 0xffff) {
+ value -= 0x10000;
+ encoded.push(toHex(value >>> 10 & 0x3ff | 0xd800));
+ value = 0xdc00 | value & 0x3ff;
+ }
+
+ encoded.push(toHex(value));
+ }
+ const srcCode = toHex(parseInt(key));
+ const dstString = encoded.join(' ');
+
+ //TODO: not good
+ entries += `<${srcCode}> <${dstString}>\n`;
+ }
+
+ return entries;
+ };
+
+ cmap.end(`\
+/CIDInit /ProcSet findresource begin
+12 dict begin
+begincmap
+/CIDSystemInfo <<
+ /Registry (Adobe)
+ /Ordering (UCS)
+ /Supplement 0
+>> def
+/CMapName /Adobe-Identity-UCS def
+/CMapType 2 def
+1 begincodespacerange
+<0000>
+endcodespacerange
+1 beginbfchar
+${characters()}
+endbfchar
+endcmap
+CMapName currentdict /CMap defineresource pop
+end
+end\
+`);
+
+ return cmap;
+ }
+}
+
+export default PDFEmbeddedFont;
\ No newline at end of file
diff --git a/src/pdf-kit-extensions/StandardFonts.js b/src/pdf-kit-extensions/StandardFonts.js
new file mode 100644
index 000000000..8d8a77785
--- /dev/null
+++ b/src/pdf-kit-extensions/StandardFonts.js
@@ -0,0 +1,64 @@
+var fs = require('fs');
+
+export const isStandardFont = (name) => {
+ return name in STANDARD_FONTS;
+};
+
+export const STANDARD_FONTS = {
+ Courier() {
+ return fs.readFileSync(__dirname + '/data/Courier.afm', 'utf8');
+ },
+
+ 'Courier-Bold'() {
+ return fs.readFileSync(__dirname + '/data/Courier-Bold.afm', 'utf8');
+ },
+
+ 'Courier-Oblique'() {
+ return fs.readFileSync(__dirname + '/data/Courier-Oblique.afm', 'utf8');
+ },
+
+ 'Courier-BoldOblique'() {
+ return fs.readFileSync(__dirname + '/data/Courier-BoldOblique.afm', 'utf8');
+ },
+
+ Helvetica() {
+ return fs.readFileSync(__dirname + '/data/Helvetica.afm', 'utf8');
+ },
+
+ 'Helvetica-Bold'() {
+ return fs.readFileSync(__dirname + '/data/Helvetica-Bold.afm', 'utf8');
+ },
+
+ 'Helvetica-Oblique'() {
+ return fs.readFileSync(__dirname + '/data/Helvetica-Oblique.afm', 'utf8');
+ },
+
+ 'Helvetica-BoldOblique'() {
+ return fs.readFileSync(__dirname + '/data/Helvetica-BoldOblique.afm', 'utf8');
+ },
+
+ 'Times-Roman'() {
+ return fs.readFileSync(__dirname + '/data/Times-Roman.afm', 'utf8');
+ },
+
+ 'Times-Bold'() {
+ return fs.readFileSync(__dirname + '/data/Times-Bold.afm', 'utf8');
+ },
+
+ 'Times-Italic'() {
+ return fs.readFileSync(__dirname + '/data/Times-Italic.afm', 'utf8');
+ },
+
+ 'Times-BoldItalic'() {
+ return fs.readFileSync(__dirname + '/data/Times-BoldItalic.afm', 'utf8');
+ },
+
+ Symbol() {
+ return fs.readFileSync(__dirname + '/data/Symbol.afm', 'utf8');
+ },
+
+ ZapfDingbats() {
+ return fs.readFileSync(__dirname + '/data/ZapfDingbats.afm', 'utf8');
+ }
+
+};
\ No newline at end of file