diff --git a/Gruntfile.js b/Gruntfile.js index d771598a5..ef58385df 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -125,7 +125,7 @@ module.exports = function(grunt) { grunt.loadNpmTasks('grunt-dump-dir'); grunt.loadNpmTasks('grunt-contrib-concat'); - grunt.registerTask('test', [ 'replace:exposeTestMethods', 'jshint', 'mochacov', 'replace:hideTestMethods' ]); + grunt.registerTask('test', [ 'replace:fixPdfKit', 'replace:exposeTestMethods', 'jshint', 'mochacov', 'replace:hideTestMethods' ]); grunt.registerTask('fixVfsFonts', 'Adds semicolon to the end of vfs_fonts.js', function () { var file = grunt.file.read('build/vfs_fonts.js'); diff --git a/src/fontProvider.js b/src/fontProvider.js new file mode 100644 index 000000000..47c6f22b4 --- /dev/null +++ b/src/fontProvider.js @@ -0,0 +1,61 @@ +/* jslint node: true */ +'use strict'; + +var _ = require('lodash'); +var FontWrapper = require('./fontWrapper'); + +function typeName(bold, italics){ + var type = 'normal'; + if (bold && italics) type = 'bolditalics'; + else if (bold) type = 'bold'; + else if (italics) type = 'italics'; + return type; +} + +function FontProvider(fontDescriptors, pdfDoc) { + this.fonts = {}; + this.pdfDoc = pdfDoc; + this.fontWrappers = {}; + + for(var font in fontDescriptors) { + if (fontDescriptors.hasOwnProperty(font)) { + var fontDef = fontDescriptors[font]; + + this.fonts[font] = { + normal: fontDef.normal, + bold: fontDef.bold, + italics: fontDef.italics, + bolditalics: fontDef.bolditalics + }; + } + } +} + +FontProvider.prototype.provideFont = function(familyName, bold, italics) { + if (!this.fonts[familyName]) return this.pdfDoc._font; + var type = typeName(bold, italics); + + this.fontWrappers[familyName] = this.fontWrappers[familyName] || {}; + + if (!this.fontWrappers[familyName][type]) { + this.fontWrappers[familyName][type] = new FontWrapper(this.pdfDoc, this.fonts[familyName][type], familyName + '(' + type + ')'); + } + + return this.fontWrappers[familyName][type]; +}; + +FontProvider.prototype.setFontRefsToPdfDoc = function(){ + var self = this; + + _.each(self.fontWrappers, function(fontFamily) { + _.each(fontFamily, function(fontWrapper){ + _.each(fontWrapper.pdfFonts, function(font){ + if (!self.pdfDoc.page.fonts[font.id]) { + self.pdfDoc.page.fonts[font.id] = font.ref(); + } + }); + }); + }); +}; + +module.exports = FontProvider; diff --git a/src/fontWrapper.js b/src/fontWrapper.js new file mode 100644 index 000000000..ae9f91e49 --- /dev/null +++ b/src/fontWrapper.js @@ -0,0 +1,106 @@ +/* jslint node: true */ +'use strict'; + +var _ = require('lodash'); + +function FontWrapper(pdfkitDoc, path, fontName){ + this.MAX_CHAR_TYPES = 92; + + this.pdfkitDoc = pdfkitDoc; + this.path = path; + this.pdfFonts = []; + this.charCatalogue = []; + this.name = fontName; + + this.__defineGetter__('ascender', function(){ + var font = this.getFont(0); + return font.ascender; + }); + this.__defineGetter__('decender', function(){ + var font = this.getFont(0); + return font.decender; + }); + +} +// private + +FontWrapper.prototype.getFont = function(index){ + if(!this.pdfFonts[index]){ + + var pseudoName = this.name + index; + + if(this.postscriptName){ + delete this.pdfkitDoc._fontFamilies[this.postscriptName]; + } + + this.pdfFonts[index] = this.pdfkitDoc.font(this.path, pseudoName)._font; + if(!this.postscriptName){ + this.postscriptName = this.pdfFonts[index].name; + } + } + + return this.pdfFonts[index]; +}; + +// public +FontWrapper.prototype.widthOfString = function(){ + var font = this.getFont(0); + return font.widthOfString.apply(font, arguments); +}; + +FontWrapper.prototype.lineHeight = function(){ + var font = this.getFont(0); + return font.lineHeight.apply(font, arguments); +}; + +FontWrapper.prototype.ref = function(){ + var font = this.getFont(0); + return font.ref.apply(font, arguments); +}; + +var toCharCode = function(char){ + return char.charCodeAt(0); +}; + +FontWrapper.prototype.encode = function(text){ + var self = this; + + var charTypesInInline = _.chain(text.split('')).map(toCharCode).uniq().value(); + if (charTypesInInline.length > self.MAX_CHAR_TYPES) { + throw new Error('Inline has more than '+ self.MAX_CHAR_TYPES + ': ' + text + ' different character types and therefore cannot be properly embedded into pdf.'); + } + + + var characterFitInFontWithIndex = function (charCatalogue) { + + return _.uniq(charCatalogue.concat(charTypesInInline)).length <= self.MAX_CHAR_TYPES; + }; + + var index = _.findIndex(self.charCatalogue, characterFitInFontWithIndex); + + if(index < 0){ + index = self.charCatalogue.length; + self.charCatalogue[index] = []; + } + + var font = this.getFont(index); + font.use(text); + + _.each(charTypesInInline, function(charCode){ + if(!_.includes(self.charCatalogue[index], charCode)){ + self.charCatalogue[index].push(charCode); + } + }); + + var encodedText = _.map(font.encode(text), function (char) { + return char.charCodeAt(0).toString(16); + }).join(''); + + return { + encodedText: encodedText, + fontId: font.id + }; +}; + + +module.exports = FontWrapper; diff --git a/src/printer.js b/src/printer.js index a5fe20abb..2816c2ed5 100644 --- a/src/printer.js +++ b/src/printer.js @@ -2,6 +2,8 @@ /* global window */ 'use strict'; +var _ = require('lodash'); +var FontProvider = require('./fontProvider'); var LayoutBuilder = require('./layoutBuilder'); var PdfKit = require('pdfkit'); var PDFReference = require('pdfkit/js/reference'); @@ -219,8 +221,6 @@ function renderPages(pages, fontProvider, pdfKitDoc) { pdfKitDoc.addPage(pdfKitDoc.options); } - setFontRefs(fontProvider, pdfKitDoc); - var page = pages[i]; for(var ii = 0, il = page.items.length; ii < il; ii++) { var item = page.items[ii]; @@ -237,24 +237,11 @@ function renderPages(pages, fontProvider, pdfKitDoc) { } } if(page.watermark){ - renderWatermark(page, pdfKitDoc, fontProvider); - } + renderWatermark(page, pdfKitDoc); } -} - -function setFontRefs(fontProvider, pdfKitDoc) { - for(var fontName in fontProvider.cache) { - var desc = fontProvider.cache[fontName]; - for (var fontType in desc) { - var font = desc[fontType]; - var _ref, _base, _name; - - if (!(_ref = (_base = pdfKitDoc.page.fonts)[_name = font.id])) { - _base[_name] = font.ref(); - } - } - } + fontProvider.setFontRefsToPdfDoc(); + } } function renderLine(line, x, y, pdfKitDoc) { @@ -262,7 +249,6 @@ function renderLine(line, x, y, pdfKitDoc) { y = y || 0; var ascenderHeight = line.getAscenderHeight(); - var lineHeight = line.getHeight(); textDecorator.drawBackground(line, x, y, pdfKitDoc); @@ -275,13 +261,14 @@ function renderLine(line, x, y, pdfKitDoc) { pdfKitDoc.save(); pdfKitDoc.transform(1, 0, 0, -1, 0, pdfKitDoc.page.height); + + var encoded = inline.font.encode(inline.text); pdfKitDoc.addContent('BT'); - var a = (inline.font.ascender / 1000 * inline.fontSize); pdfKitDoc.addContent('' + (x + inline.x) + ' ' + (pdfKitDoc.page.height - y - ascenderHeight) + ' Td'); - pdfKitDoc.addContent('/' + inline.font.id + ' ' + inline.fontSize + ' Tf'); + pdfKitDoc.addContent('/' + encoded.fontId + ' ' + inline.fontSize + ' Tf'); - pdfKitDoc.addContent('<' + encode(inline.font, inline.text) + '> Tj'); + pdfKitDoc.addContent('<' + encoded.encodedText + '> Tj'); pdfKitDoc.addContent('ET'); pdfKitDoc.restore(); @@ -291,7 +278,7 @@ function renderLine(line, x, y, pdfKitDoc) { } -function renderWatermark(page, pdfKitDoc, fontProvider){ +function renderWatermark(page, pdfKitDoc){ var watermark = page.watermark; pdfKitDoc.fill('black'); @@ -303,30 +290,15 @@ function renderWatermark(page, pdfKitDoc, fontProvider){ var angle = Math.atan2(pdfKitDoc.page.height, pdfKitDoc.page.width) * 180/Math.PI; pdfKitDoc.rotate(angle, {origin: [pdfKitDoc.page.width/2, pdfKitDoc.page.height/2]}); + var encoded = watermark.font.encode(watermark.text); pdfKitDoc.addContent('BT'); pdfKitDoc.addContent('' + (pdfKitDoc.page.width/2 - watermark.size.size.width/2) + ' ' + (pdfKitDoc.page.height/2 - watermark.size.size.height/4) + ' Td'); - pdfKitDoc.addContent('/' + watermark.font.id + ' ' + watermark.size.fontSize + ' Tf'); - pdfKitDoc.addContent('<' + encode(watermark.font, watermark.text) + '> Tj'); + pdfKitDoc.addContent('/' + encoded.fontId + ' ' + watermark.size.fontSize + ' Tf'); + pdfKitDoc.addContent('<' + encoded.encodedText + '> Tj'); pdfKitDoc.addContent('ET'); pdfKitDoc.restore(); } -function encode(font, text) { - font.use(text); - - text = font.encode(text); - text = ((function() { - var _results = []; - - for (var i = 0, _ref2 = text.length; 0 <= _ref2 ? i < _ref2 : i > _ref2; 0 <= _ref2 ? i++ : i--) { - _results.push(text.charCodeAt(i).toString(16)); - } - return _results; - })()).join(''); - - return text; -} - function renderVector(vector, pdfDoc) { //TODO: pdf optimization (there's no need to write all properties everytime) pdfDoc.lineWidth(vector.lineWidth || 1); @@ -388,45 +360,6 @@ function renderImage(image, x, y, pdfKitDoc) { pdfKitDoc.image(image.image, image.x, image.y, { width: image._width, height: image._height }); } -function FontProvider(fontDescriptors, pdfDoc) { - this.fonts = {}; - this.pdfDoc = pdfDoc; - this.cache = {}; - - for(var font in fontDescriptors) { - if (fontDescriptors.hasOwnProperty(font)) { - var fontDef = fontDescriptors[font]; - - this.fonts[font] = { - normal: fontDef.normal, - bold: fontDef.bold, - italics: fontDef.italics, - bolditalics: fontDef.bolditalics - }; - } - } -} - -FontProvider.prototype.provideFont = function(familyName, bold, italics) { - if (!this.fonts[familyName]) return this.pdfDoc._font; - - var type = 'normal'; - - if (bold && italics) type = 'bolditalics'; - else if (bold) type = 'bold'; - else if (italics) type = 'italics'; - - if (!this.cache[familyName]) this.cache[familyName] = {}; - - var cached = this.cache[familyName] && this.cache[familyName][type]; - - if (cached) return cached; - - var fontCache = (this.cache[familyName] = this.cache[familyName] || {}); - fontCache[type] = this.pdfDoc.font(this.fonts[familyName][type], familyName + ' (' + type + ')')._font; - return fontCache[type]; -}; - module.exports = PdfPrinter; diff --git a/tests/fontWrapper.js b/tests/fontWrapper.js new file mode 100644 index 000000000..6b4da2bdc --- /dev/null +++ b/tests/fontWrapper.js @@ -0,0 +1,117 @@ +var assert = require('assert'); +var _ = require('lodash'); + +var FontWrapper = require('../src/fontWrapper'); +var PdfKit = require('pdfkit'); + +describe('FontWrapper', function() { + var fontWrapper, pdfDoc; + + + function getEncodedUnicodes(index, pdfDoc){ + return pdfDoc.font('Roboto (bolditalic)' + index)._font.subset.unicodes; + } + + beforeEach(function() { + pdfDoc = new PdfKit({ size: [ 595.28, 841.89 ], compress: false}); + fontWrapper = new FontWrapper(pdfDoc, 'examples/fonts/Roboto-Italic.ttf', 'Roboto (bolditalic)') + }); + + describe('encoding', function() { + + + + + it('encodes text', function () { + var encoded = fontWrapper.encode('Anna '); + + // A n n a + // 21 22 22 23 24 + assert.equal(encoded.encodedText, '2122222324'); + assert.equal(encoded.fontId, 'F1'); + assert.equal(pdfDoc._fontCount, 1); + var encodedUnicodes = getEncodedUnicodes(0, pdfDoc); + assert.equal(encodedUnicodes['A'.charCodeAt(0)], 33); + assert.equal(encodedUnicodes['n'.charCodeAt(0)], 34); + assert.equal(encodedUnicodes['a'.charCodeAt(0)], 35); + assert.equal(encodedUnicodes[' '.charCodeAt(0)], 36); + }); + + it('encodes text and re-use characters', function () { + fontWrapper.encode('Anna '); + var encoded = fontWrapper.encode('na na AAA!'); + + // A n n a + // 21 22 22 23 24 + + // n a n a A A A ! + // 22 23 24 22 23 24 21 21 21 25 + assert.equal(encoded.encodedText, '22232422232421212125'); + assert.equal(encoded.fontId, 'F1'); + assert.equal(pdfDoc._fontCount, 1); + var encodedUnicodes = getEncodedUnicodes(0, pdfDoc); + assert.equal(encodedUnicodes['A'.charCodeAt(0)], 33); + assert.equal(encodedUnicodes['n'.charCodeAt(0)], 34); + assert.equal(encodedUnicodes['a'.charCodeAt(0)], 35); + assert.equal(encodedUnicodes[' '.charCodeAt(0)], 36); + assert.equal(encodedUnicodes['!'.charCodeAt(0)], 37); + }); + + it('encodes in new font when old font is used', function () { + var text = _.times(91, String.fromCharCode).join(''); // does not include a-z, includes 0-9 & A-Z + fontWrapper.encode(text); + var encoded = fontWrapper.encode('cannot'); + + // c a n n o t + // 21 22 23 23 24 25 + assert.equal(encoded.encodedText, '212223232425'); + assert.equal(encoded.fontId, 'F2'); + assert.equal(pdfDoc._fontCount, 2); + var encodedUnicodes = getEncodedUnicodes(1, pdfDoc); + assert.equal(encodedUnicodes['c'.charCodeAt(0)], 33); + assert.equal(encodedUnicodes['a'.charCodeAt(0)], 34); + assert.equal(encodedUnicodes['n'.charCodeAt(0)], 35); + assert.equal(encodedUnicodes['o'.charCodeAt(0)], 36); + assert.equal(encodedUnicodes['t'.charCodeAt(0)], 37); + }); + + it('encodes NOT in new font when both unique character sets are equal', function () { + var text1 = _.times(47, String.fromCharCode).join(''); // does not include a-z, includes 0-9 & A-Z + fontWrapper.encode(text1); + + var text2 = _.times(47, String.fromCharCode).join(''); // does not include a-z, includes 0-9 & A-Z + fontWrapper.encode(text2); + + assert.equal(pdfDoc._fontCount, 1); + assert.equal(pdfDoc._font.id, 'F1'); + assert.equal(pdfDoc._font.name, 'Roboto-Italic'); + }); + + it('use other font when it still has enough space', function () { + var text1 = 'cannot', + text2 = _.times(92, String.fromCharCode).join(''), // does not include a-z, includes 0-9 & A-Z + text3 = 'This can work.'; + + + fontWrapper.encode(text1); + fontWrapper.encode(text2); + var encoded = fontWrapper.encode(text3); + + // c a n n o t + // 21 22 23 23 24 25 + + // T h i s c a n w o r k . + // 26 27 28 29 2a 21 22 23 2a 2b 24 2c 2d 2e + assert.equal(encoded.encodedText, '262728292a2122232a2b242c2d2e'); + assert.equal(encoded.fontId, 'F1'); + assert.equal(pdfDoc._fontCount, 2); + var encodedUnicodes = getEncodedUnicodes(0, pdfDoc); + assert.equal(encodedUnicodes['T'.charCodeAt(0)], 38); + assert.equal(encodedUnicodes['h'.charCodeAt(0)], 39); + assert.equal(encodedUnicodes['i'.charCodeAt(0)], 40); + assert.equal(encodedUnicodes['s'.charCodeAt(0)], 41); + assert.equal(encodedUnicodes[' '.charCodeAt(0)], 42); + }); + + }); +}); diff --git a/tests/printer.js b/tests/printer.js index d2094bbb1..c23170721 100644 --- a/tests/printer.js +++ b/tests/printer.js @@ -131,4 +131,50 @@ describe('Printer', function () { assert.deepEqual(Pdfkit.prototype.addPage.thirdCall.args[0].size, [LONG_SIDE, SHORT_SIDE]); }); -}); \ No newline at end of file + it('should print bullet vectors as ellipses', function () { + printer = new Printer(fontDescriptors); + var docDefinition = { + pageOrientation: 'portrait', + pageSize: { width: SHORT_SIDE, height: LONG_SIDE }, + content: [ + { + "stack": [ + { + "ul": [ + { "text": "item1" }, + { "text": "item2" } + ] + } + ] + } + ] + }; + Pdfkit.prototype.ellipse = sinon.spy(Pdfkit.prototype.ellipse); + + printer.createPdfKitDocument(docDefinition); + + function assertEllipse(ellipseCallArgs) { + var firstEllipse = { + x: ellipseCallArgs[0], + y: ellipseCallArgs[1], + r1: ellipseCallArgs[2], + r2: ellipseCallArgs[3] + }; + assert(firstEllipse.x !== undefined); + assert(! isNaN(firstEllipse.x)); + assert(firstEllipse.y !== undefined); + assert(! isNaN(firstEllipse.y)); + assert(firstEllipse.r1 !== undefined); + assert(! isNaN(firstEllipse.r1)); + assert(firstEllipse.r2 !== undefined); + assert(! isNaN(firstEllipse.r2)); + } + + assert.equal(Pdfkit.prototype.ellipse.callCount, 2); + + assertEllipse(Pdfkit.prototype.ellipse.firstCall.args); + assertEllipse(Pdfkit.prototype.ellipse.secondCall.args); + + }); + +});