diff --git a/src/canvas.js b/src/canvas.js index 0765049a9..ff14e5d1d 100644 --- a/src/canvas.js +++ b/src/canvas.js @@ -979,29 +979,45 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { var width = vmetric ? -vmetric[0] : glyph.width; var charWidth = width * fontSize * current.fontMatrix[0] + charSpacing * current.fontDirection; + var accent = glyph.accent; + var scaledX, scaledY, scaledAccentX, scaledAccentY; if (!glyph.disabled) { if (vertical) { - var scaledX = vx / fontSizeScale; - var scaledY = (x + vy) / fontSizeScale; + scaledX = vx / fontSizeScale; + scaledY = (x + vy) / fontSizeScale; } else { - var scaledX = x / fontSizeScale; - var scaledY = 0; + scaledX = x / fontSizeScale; + scaledY = 0; + } + if (accent) { + scaledAccentX = scaledX + accent.offset.x / fontSizeScale; + scaledAccentY = scaledY - accent.offset.y / fontSizeScale; } switch (textRenderingMode) { default: // other unsupported rendering modes case TextRenderingMode.FILL: case TextRenderingMode.FILL_ADD_TO_PATH: ctx.fillText(character, scaledX, scaledY); + if (accent) { + ctx.fillText(accent.fontChar, scaledAccentX, scaledAccentY); + } break; case TextRenderingMode.STROKE: case TextRenderingMode.STROKE_ADD_TO_PATH: ctx.strokeText(character, scaledX, scaledY); + if (accent) { + ctx.strokeText(accent.fontChar, scaledAccentX, scaledAccentY); + } break; case TextRenderingMode.FILL_STROKE: case TextRenderingMode.FILL_STROKE_ADD_TO_PATH: ctx.fillText(character, scaledX, scaledY); ctx.strokeText(character, scaledX, scaledY); + if (accent) { + ctx.fillText(accent.fontChar, scaledAccentX, scaledAccentY); + ctx.strokeText(accent.fontChar, scaledAccentX, scaledAccentY); + } break; case TextRenderingMode.INVISIBLE: case TextRenderingMode.ADD_TO_PATH: @@ -1010,6 +1026,9 @@ var CanvasGraphics = (function CanvasGraphicsClosure() { if (textRenderingMode & TextRenderingMode.ADD_TO_PATH_FLAG) { var clipCtx = this.getCurrentTextClipping(); clipCtx.fillText(character, scaledX, scaledY); + if (accent) { + clipCtx.fillText(accent.fontChar, scaledAccentX, scaledAccentY); + } } } diff --git a/src/fonts.js b/src/fonts.js index 0e7424d17..ef3463270 100644 --- a/src/fonts.js +++ b/src/fonts.js @@ -34,6 +34,10 @@ var PDF_GLYPH_SPACE_UNITS = 1000; // in tracemonkey and various other pdfs with type1 fonts. var HINTING_ENABLED = false; +// Accented charactars are not displayed properly on windows, using this flag +// to control analysis of seac charstrings. +var SEAC_ANALYSIS_ENABLED = false; + var FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0]; var FontFlags = { @@ -2445,6 +2449,7 @@ var Font = (function FontClosure() { this.widths = properties.widths; this.defaultWidth = properties.defaultWidth; this.encoding = properties.baseEncoding; + this.seacMap = properties.seacMap; this.loading = true; } @@ -4167,6 +4172,36 @@ var Font = (function FontClosure() { } this.glyphNameMap = glyphNameMap; + var seacs = font.seacs; + if (SEAC_ANALYSIS_ENABLED && seacs) { + var seacMap = []; + var matrix = properties.fontMatrix || FONT_IDENTITY_MATRIX; + for (var i = 0; i < charstrings.length; ++i) { + var charstring = charstrings[i]; + var seac = seacs[charstring.gid]; + if (!seac) { + continue; + } + var baseGlyphName = Encodings.StandardEncoding[seac[2]]; + var baseUnicode = glyphNameMap[baseGlyphName]; + var accentGlyphName = Encodings.StandardEncoding[seac[3]]; + var accentUnicode = glyphNameMap[accentGlyphName]; + if (!baseUnicode || !accentUnicode) { + continue; + } + var accentOffset = { + x: seac[0] * matrix[0] + seac[1] * matrix[2] + matrix[4], + y: seac[0] * matrix[1] + seac[1] * matrix[3] + matrix[5] + }; + seacMap[charstring.unicode] = { + baseUnicode: baseUnicode, + accentUnicode: accentUnicode, + accentOffset: accentOffset + }; + } + properties.seacMap = seacMap; + } + if (!properties.hasEncoding && (properties.subtype == 'Type1C' || properties.subtype == 'CIDFontType0C')) { var encoding = []; @@ -4515,16 +4550,28 @@ var Font = (function FontClosure() { var unicodeChars = !('toUnicode' in this) ? charcode : this.toUnicode[charcode] || charcode; - if (typeof unicodeChars === 'number') + if (typeof unicodeChars === 'number') { unicodeChars = String.fromCharCode(unicodeChars); + } width = isNum(width) ? width : this.defaultWidth; disabled = this.unicodeIsEnabled ? !this.unicodeIsEnabled[fontCharCode] : false; + var accent = null; + if (this.seacMap && this.seacMap[fontCharCode]) { + var seac = this.seacMap[fontCharCode]; + fontCharCode = seac.baseUnicode; + accent = { + fontChar: String.fromCharCode(seac.accentUnicode), + offset: seac.accentOffset + }; + } + return { fontChar: String.fromCharCode(fontCharCode), unicode: unicodeChars, + accent: accent, width: width, vmetric: vmetric, disabled: disabled, @@ -4807,7 +4854,12 @@ var Type1CharString = (function Type1CharStringClosure() { case (12 << 8) + 6: // seac // seac is like type 2's special endchar but it doesn't use the // first argument asb, so remove it. - error = this.executeCommand(4, COMMAND_MAP.endchar); + if (SEAC_ANALYSIS_ENABLED) { + this.seac = this.stack.splice(-4, 4); + error = this.executeCommand(0, COMMAND_MAP.endchar); + } else { + error = this.executeCommand(4, COMMAND_MAP.endchar); + } break; case (12 << 8) + 7: // sbw if (this.stack.length < 4) { @@ -5167,6 +5219,7 @@ var Type1Parser = function type1Parser() { program.charstrings.push({ glyph: glyph, data: output, + seac: charString.seac, lsb: charString.lsb, width: charString.width }); @@ -5333,6 +5386,7 @@ var Type1Font = function Type1Font(name, file, properties) { this.charstrings = charstrings; this.data = this.wrap(name, type2Charstrings, this.charstrings, subrs, properties); + this.seacs = this.getSeacs(data.charstrings); }; Type1Font.prototype = { @@ -5362,6 +5416,18 @@ Type1Font.prototype = { return charstrings; }, + getSeacs: function Type1Font_getSeacs(charstrings) { + var i, ii; + var seacMap = []; + for (i = 0, ii = charstrings.length; i < ii; i++) { + var charstring = charstrings[i]; + if (charstring.seac) { + seacMap[i] = charstring.seac; + } + } + return seacMap; + }, + getType2Charstrings: function Type1Font_getType2Charstrings( type1Charstrings) { var type2Charstrings = []; @@ -5517,6 +5583,7 @@ var CFFFont = (function CFFFontClosure() { this.charstrings = charstrings; this.glyphIds = glyphIds; + this.seacs = cff.seacs; }, getCharStrings: function CFFFont_getCharStrings(charsets, encoding) { var charstrings = []; @@ -5619,11 +5686,27 @@ var CFFParser = (function CFFParserClosure() { null, null, { id: 'abs', min: 1, stackDelta: 0 }, - { id: 'add', min: 2, stackDelta: -1 }, - { id: 'sub', min: 2, stackDelta: -1 }, - { id: 'div', min: 2, stackDelta: -1 }, + { id: 'add', min: 2, stackDelta: -1, + stackFn: function stack_div(stack, index) { + stack[index - 2] = stack[index - 2] + stack[index - 1]; + } + }, + { id: 'sub', min: 2, stackDelta: -1, + stackFn: function stack_div(stack, index) { + stack[index - 2] = stack[index - 2] - stack[index - 1]; + } + }, + { id: 'div', min: 2, stackDelta: -1, + stackFn: function stack_div(stack, index) { + stack[index - 2] = stack[index - 2] / stack[index - 1]; + } + }, null, - { id: 'neg', min: 1, stackDelta: 0 }, + { id: 'neg', min: 1, stackDelta: 0, + stackFn: function stack_div(stack, index) { + stack[index - 1] = -stack[index - 1]; + } + }, { id: 'eq', min: 2, stackDelta: -1 }, null, null, @@ -5633,7 +5716,11 @@ var CFFParser = (function CFFParserClosure() { { id: 'get', min: 1, stackDelta: 0 }, { id: 'ifelse', min: 4, stackDelta: -3 }, { id: 'random', min: 0, stackDelta: 1 }, - { id: 'mul', min: 2, stackDelta: -1 }, + { id: 'mul', min: 2, stackDelta: -1, + stackFn: function stack_div(stack, index) { + stack[index - 2] = stack[index - 2] * stack[index - 1]; + } + }, null, { id: 'sqrt', min: 1, stackDelta: 0 }, { id: 'dup', min: 1, stackDelta: 1 }, @@ -5681,7 +5768,9 @@ var CFFParser = (function CFFParserClosure() { cff.isCIDFont = topDict.hasName('ROS'); var charStringOffset = topDict.getByName('CharStrings'); - cff.charStrings = this.parseCharStrings(charStringOffset); + var charStringsAndSeacs = this.parseCharStrings(charStringOffset); + cff.charStrings = charStringsAndSeacs.charStrings; + cff.seacs = charStringsAndSeacs.seacs; var fontMatrix = topDict.getByName('FontMatrix'); if (fontMatrix) { @@ -5892,11 +5981,13 @@ var CFFParser = (function CFFParserClosure() { }, parseCharStrings: function CFFParser_parseCharStrings(charStringOffset) { var charStrings = this.parseIndex(charStringOffset).obj; + var seacs = []; var count = charStrings.count; for (var i = 0; i < count; i++) { var charstring = charStrings.get(i); var stackSize = 0; + var stack = []; var undefStack = true; var hints = 0; var valid = true; @@ -5920,18 +6011,29 @@ var CFFParser = (function CFFParserClosure() { validationCommand = CharstringValidationData12[q]; } } else if (value === 28) { // number (16 bit) + stack[stackSize] = ((data[j] << 24) | (data[j + 1] << 16)) >> 16; j += 2; stackSize++; } else if (value == 14) { if (stackSize >= 4) { stackSize -= 4; + if (SEAC_ANALYSIS_ENABLED) { + seacs[i] = stack.slice(stackSize, stackSize + 4); + valid = false; + } } } else if (value >= 32 && value <= 246) { // number + stack[stackSize] = value - 139; stackSize++; } else if (value >= 247 && value <= 254) { // number (+1 bytes) + stack[stackSize] = value < 251 ? + ((value - 247) << 8) + data[j] + 108 : + -((value - 251) << 8) - data[j] - 108; j++; stackSize++; } else if (value == 255) { // number (32 bit) + stack[stackSize] = ((data[j] << 24) | (data[j + 1] << 16) | + (data[j + 2] << 8) | data[j + 3]) / 65536; j += 4; stackSize++; } else if (value == 19 || value == 20) { @@ -5955,6 +6057,9 @@ var CFFParser = (function CFFParserClosure() { } } if ('stackDelta' in validationCommand) { + if ('stackFn' in validationCommand) { + validationCommand.stackFn(stack, stackSize); + } stackSize += validationCommand.stackDelta; } else if (validationCommand.resetStack) { stackSize = 0; @@ -5970,7 +6075,7 @@ var CFFParser = (function CFFParserClosure() { charStrings.set(i, new Uint8Array([14])); } } - return charStrings; + return { charStrings: charStrings, seacs: seacs }; }, parsePrivateDict: function CFFParser_parsePrivateDict(parentDict) { // no private dict, do nothing @@ -6851,6 +6956,13 @@ var CFFCompiler = (function CFFCompilerClosure() { return CFFCompiler; })(); +// Workaround for seac on Windows. +(function checkSeacSupport() { + if (/Windows/.test(navigator.userAgent)) { + SEAC_ANALYSIS_ENABLED = true; + } +})(); + // Workaround for Private Use Area characters in Chrome on Windows // http://code.google.com/p/chromium/issues/detail?id=122465 // https://github.com/mozilla/pdf.js/issues/1689 diff --git a/test/unit/font_spec.js b/test/unit/font_spec.js index ccbb3525a..02a663936 100644 --- a/test/unit/font_spec.js +++ b/test/unit/font_spec.js @@ -1,6 +1,7 @@ /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */ -/* globals expect, it, describe, CFFCompiler, CFFParser, CFFIndex, CFFStrings */ +/* globals expect, it, describe, CFFCompiler, CFFParser, CFFIndex, CFFStrings, + SEAC_ANALYSIS_ENABLED:true */ 'use strict'; @@ -113,12 +114,65 @@ describe('font', function() { 14 // endchar ]); parser.bytes = bytes; - var charStrings = parser.parseCharStrings(0); + var charStrings = parser.parseCharStrings(0).charStrings; expect(charStrings.count).toEqual(1); // shoudn't be sanitized expect(charStrings.get(0).length).toEqual(38); }); + it('parses a CharString endchar with 4 args w/seac enabled', function() { + var seacAnalysisState = SEAC_ANALYSIS_ENABLED; + try { + SEAC_ANALYSIS_ENABLED = true; + var bytes = new Uint8Array([0, 1, // count + 1, // offsetSize + 0, // offset[0] + 237, 247, 22, 247, 72, 204, 247, 86, 14]); + parser.bytes = bytes; + var result = parser.parseCharStrings(0); + expect(result.charStrings.count).toEqual(1); + expect(result.charStrings.get(0).length).toEqual(1); + expect(result.seacs.length).toEqual(1); + expect(result.seacs[0].length).toEqual(4); + expect(result.seacs[0][0]).toEqual(130); + expect(result.seacs[0][1]).toEqual(180); + expect(result.seacs[0][2]).toEqual(65); + expect(result.seacs[0][3]).toEqual(194); + } finally { + SEAC_ANALYSIS_ENABLED = seacAnalysisState; + } + }); + + it('parses a CharString endchar with 4 args w/seac disabled', function() { + var seacAnalysisState = SEAC_ANALYSIS_ENABLED; + try { + SEAC_ANALYSIS_ENABLED = false; + var bytes = new Uint8Array([0, 1, // count + 1, // offsetSize + 0, // offset[0] + 237, 247, 22, 247, 72, 204, 247, 86, 14]); + parser.bytes = bytes; + var result = parser.parseCharStrings(0); + expect(result.charStrings.count).toEqual(1); + expect(result.charStrings.get(0).length).toEqual(9); + expect(result.seacs.length).toEqual(0); + } finally { + SEAC_ANALYSIS_ENABLED = seacAnalysisState; + } + }); + + it('parses a CharString endchar no args', function() { + var bytes = new Uint8Array([0, 1, // count + 1, // offsetSize + 0, // offset[0] + 14]); + parser.bytes = bytes; + var result = parser.parseCharStrings(0); + expect(result.charStrings.count).toEqual(1); + expect(result.charStrings.get(0)[0]).toEqual(14); + expect(result.seacs.length).toEqual(0); + }); + it('parses predefined charsets', function() { var charset = parser.parseCharsets(0, 0, null, true); expect(charset.predefined).toEqual(true);