Map all glyphs to the private use area and duplicate the first glyph.

There have been lots of problems with trying to map glyphs to their unicode
values. It's more reliable to just use the private use areas so the browser's
font renderer doesn't mess with the glyphs.

Using the private use area for all glyphs did highlight other issues that this
patch also had to fix:

  * small private use area - Previously, only the BMP private use area was used
    which can't map many glyphs. Now, the (much bigger) PUP 16 area can also be
    used.

  * glyph zero not shown - Browsers will not use the glyph from a font if it is
    glyph id = 0. This issue was less prevalent when we mapped to unicode values
    since the fallback font would be used. However, when using the private use
    area, the glyph would not be drawn at all. This is illustrated in one of the
    current test cases (issue #8234) where there's an "ä" glyph at position
    zero. The PDF looked like it rendered correctly, but it was actually not
    using the glyph from the font. To properly show the first glyph it is always
    duplicated and appended to the glyphs and the maps are adjusted.

  * supplementary characters - The private use area PUP 16 is 4 bytes, so
    String.fromCodePoint must be used where we previously used
    String.fromCharCode. This is actually an issue that should have been fixed
    regardless of this patch.

  * charset - Freetype fails to load fonts when the charset size doesn't match
    number of glyphs in the font. We now write out a fake charset with the
    correct length. This also brought up the issue that glyphs with seac/endchar
    should only ever write a standard charset, but we now write a custom one.
    To get around this the seac analysis is permanently enabled so those glyphs
    are instead always drawn as two glyphs.
This commit is contained in:
Brendan Dahl 2018-01-04 14:43:07 -08:00 committed by Brendan Dahl
parent 20cd1b354b
commit b76cf665ec
14 changed files with 16188 additions and 304 deletions

View File

@ -815,11 +815,10 @@ var CFFParser = (function CFFParserClosure() {
return new CFFEncoding(predefined, format, encoding, raw);
},
parseFDSelect: function CFFParser_parseFDSelect(pos, length) {
var start = pos;
var bytes = this.bytes;
var format = bytes[pos++];
var fdSelect = [], rawBytes;
var i, invalidFirstGID = false;
var fdSelect = [];
var i;
switch (format) {
case 0:
@ -827,7 +826,6 @@ var CFFParser = (function CFFParserClosure() {
var id = bytes[pos++];
fdSelect.push(id);
}
rawBytes = bytes.subarray(start, pos);
break;
case 3:
var rangesCount = (bytes[pos++] << 8) | bytes[pos++];
@ -836,7 +834,6 @@ var CFFParser = (function CFFParserClosure() {
if (i === 0 && first !== 0) {
warn('parseFDSelect: The first range must have a first GID of 0' +
' -- trying to recover.');
invalidFirstGID = true;
first = 0;
}
var fdIndex = bytes[pos++];
@ -847,11 +844,6 @@ var CFFParser = (function CFFParserClosure() {
}
// Advance past the sentinel(next).
pos += 2;
rawBytes = bytes.subarray(start, pos);
if (invalidFirstGID) {
rawBytes[3] = rawBytes[4] = 0; // Adjust the first range, first GID.
}
break;
default:
throw new FormatError(`parseFDSelect: Unknown format "${format}".`);
@ -860,7 +852,7 @@ var CFFParser = (function CFFParserClosure() {
throw new FormatError('parseFDSelect: Invalid font data.');
}
return new CFFFDSelect(fdSelect, rawBytes);
return new CFFFDSelect(format, fdSelect);
},
};
return CFFParser;
@ -885,6 +877,30 @@ var CFF = (function CFFClosure() {
this.isCIDFont = false;
}
CFF.prototype = {
duplicateFirstGlyph: function CFF_duplicateFirstGlyph() {
// Browsers will not display a glyph at position 0. Typically glyph 0 is
// notdef, but a number of fonts put a valid glyph there so it must be
// duplicated and appended.
if (this.charStrings.count >= 65535) {
warn('Not enough space in charstrings to duplicate first glyph.');
return;
}
var glyphZero = this.charStrings.get(0);
this.charStrings.add(glyphZero);
if (this.isCIDFont) {
this.fdSelect.fdSelect.push(this.fdSelect.fdSelect[0]);
}
},
hasGlyphId: function CFF_hasGlyphID(id) {
if (id < 0 || id >= this.charStrings.count) {
return false;
}
var glyph = this.charStrings.get(id);
return glyph.length > 0;
},
};
return CFF;
})();
@ -1142,9 +1158,9 @@ var CFFEncoding = (function CFFEncodingClosure() {
})();
var CFFFDSelect = (function CFFFDSelectClosure() {
function CFFFDSelect(fdSelect, raw) {
function CFFFDSelect(format, fdSelect) {
this.format = format;
this.fdSelect = fdSelect;
this.raw = raw;
}
CFFFDSelect.prototype = {
getFDIndex: function CFFFDSelect_get(glyphIndex) {
@ -1261,6 +1277,7 @@ var CFFCompiler = (function CFFCompilerClosure() {
}
}
cff.topDict.setByName('charset', 0);
var compiled = this.compileTopDicts([cff.topDict],
output.length,
cff.isCIDFont);
@ -1284,17 +1301,9 @@ var CFFCompiler = (function CFFCompilerClosure() {
output.add(encoding);
}
}
if (cff.charset && cff.topDict.hasName('charset')) {
if (cff.charset.predefined) {
topDictTracker.setEntryLocation('charset', [cff.charset.format],
output);
} else {
var charset = this.compileCharset(cff.charset);
topDictTracker.setEntryLocation('charset', [output.length], output);
output.add(charset);
}
}
var charStrings = this.compileCharStrings(cff.charStrings);
topDictTracker.setEntryLocation('CharStrings', [output.length], output);
@ -1304,7 +1313,7 @@ var CFFCompiler = (function CFFCompilerClosure() {
// For some reason FDSelect must be in front of FDArray on windows. OSX
// and linux don't seem to care.
topDictTracker.setEntryLocation('FDSelect', [output.length], output);
var fdSelect = this.compileFDSelect(cff.fdSelect.raw);
var fdSelect = this.compileFDSelect(cff.fdSelect);
output.add(fdSelect);
// It is unclear if the sub font dictionary can have CID related
// dictionary keys, but the sanitizer doesn't like them so remove them.
@ -1547,16 +1556,68 @@ var CFFCompiler = (function CFFCompilerClosure() {
this.out.writeByteArray(this.compileIndex(globalSubrIndex));
},
compileCharStrings: function CFFCompiler_compileCharStrings(charStrings) {
return this.compileIndex(charStrings);
var charStringsIndex = new CFFIndex();
for (var i = 0; i < charStrings.count; i++) {
var glyph = charStrings.get(i);
// If the CharString outline is empty, replace it with .notdef to
// prevent OTS from rejecting the font (fixes bug1252420.pdf).
if (glyph.length === 0) {
charStringsIndex.add(new Uint8Array([0x8B, 0x0E]));
continue;
}
charStringsIndex.add(glyph);
}
return this.compileIndex(charStringsIndex);
},
compileCharset: function CFFCompiler_compileCharset(charset) {
return this.compileTypedArray(charset.raw);
let length = 1 + (this.cff.charStrings.count - 1) * 2;
// The contents of the charset doesn't matter, it's just there to make
// freetype happy.
let out = new Uint8Array(length);
return this.compileTypedArray(out);
},
compileEncoding: function CFFCompiler_compileEncoding(encoding) {
return this.compileTypedArray(encoding.raw);
},
compileFDSelect: function CFFCompiler_compileFDSelect(fdSelect) {
return this.compileTypedArray(fdSelect);
let format = fdSelect.format;
let out, i;
switch (format) {
case 0:
out = new Uint8Array(1 + fdSelect.fdSelect.length);
out[0] = format;
for (i = 0; i < fdSelect.fdSelect.length; i++) {
out[i + 1] = fdSelect.fdSelect[i];
}
break;
case 3:
let start = 0;
let lastFD = fdSelect.fdSelect[0];
let ranges = [
format,
0, // nRanges place holder
0, // nRanges place holder
(start >> 8) & 0xFF,
start & 0xFF,
lastFD
];
for (i = 1; i < fdSelect.fdSelect.length; i++) {
let currentFD = fdSelect.fdSelect[i];
if (currentFD !== lastFD) {
ranges.push((i >> 8) & 0xFF, i & 0xFF, currentFD);
lastFD = currentFD;
}
}
// 3 bytes are pushed for every range and there are 3 header bytes.
let numRanges = (ranges.length - 3) / 3;
ranges[1] = (numRanges >> 8) & 0xFF;
ranges[2] = numRanges & 0xFF;
// sentinel
ranges.push((i >> 8) & 0xFF, i & 0xFF);
out = new Uint8Array(ranges);
break;
}
return this.compileTypedArray(out);
},
compileTypedArray: function CFFCompiler_compileTypedArray(data) {
var out = [];
@ -1648,4 +1709,5 @@ export {
CFFTopDict,
CFFPrivateDict,
CFFCompiler,
CFFFDSelect,
};

View File

@ -122,7 +122,7 @@ var FontRendererFactory = (function FontRendererFactoryClosure() {
}
function lookupCmap(ranges, unicode) {
var code = unicode.charCodeAt(0), gid = 0;
var code = unicode.codePointAt(0), gid = 0;
var l = 0, r = ranges.length - 1;
while (l < r) {
var c = (l + r + 1) >> 1;

View File

@ -39,18 +39,24 @@ import { IdentityCMap } from './cmap';
import { Stream } from './stream';
import { Type1Parser } from './type1_parser';
// Unicode Private Use Area
var PRIVATE_USE_OFFSET_START = 0xE000;
var PRIVATE_USE_OFFSET_END = 0xF8FF;
var SKIP_PRIVATE_USE_RANGE_F000_TO_F01F = false;
// Unicode Private Use Areas:
const PRIVATE_USE_AREAS = [
[0xE000, 0xF8FF], // BMP (0)
[0x100000, 0x10FFFD], // PUP (16)
];
// PDF Glyph Space Units are one Thousandth of a TextSpace Unit
// except for Type 3 fonts
var PDF_GLYPH_SPACE_UNITS = 1000;
// Accented characters are not displayed properly on Windows, using this flag
// to control analysis of seac charstrings.
var SEAC_ANALYSIS_ENABLED = false;
// Accented characters have issues on Windows and Linux. When this flag is
// enabled glyphs that use seac and seac style endchar operators are truncated
// and we instead just store the glyph id's of the base glyph and its accent to
// be drawn individually.
// Linux (freetype) requires that when a seac style endchar is used
// that the charset must be a predefined one, however we build a
// custom one. Windows just refuses to draw glyphs with seac operators.
var SEAC_ANALYSIS_ENABLED = true;
var FontFlags = {
FixedPitch: 1,
@ -444,37 +450,6 @@ var OpenTypeFileBuilder = (function OpenTypeFileBuilderClosure() {
return OpenTypeFileBuilder;
})();
// Problematic Unicode characters in the fonts that needs to be moved to avoid
// issues when they are painted on the canvas, e.g. complex-script shaping or
// control/whitespace characters. The ranges are listed in pairs: the first item
// is a code of the first problematic code, the second one is the next
// non-problematic code. The ranges must be in sorted order.
var ProblematicCharRanges = new Int32Array([
// Control characters.
0x0000, 0x0020,
0x007F, 0x00A1,
0x00AD, 0x00AE,
// Chars that is used in complex-script shaping.
0x0600, 0x0780,
0x08A0, 0x10A0,
0x1780, 0x1800,
0x1C00, 0x1C50,
// General punctuation chars.
0x2000, 0x2010,
0x2011, 0x2012,
0x2028, 0x2030,
0x205F, 0x2070,
0x25CC, 0x25CD,
0x3000, 0x3001,
0x3164, 0x3165,
// Chars that is used in complex-script shaping.
0xAA60, 0xAA80,
// Unicode high surrogates.
0xD800, 0xE000,
// Specials Unicode block.
0xFFF0, 0x10000
]);
/**
* 'Font' is the class the outside world should use, it encapsulate all the font
* decoding logics whatever type it is (assuming the font type is supported).
@ -755,91 +730,46 @@ var Font = (function FontClosure() {
}
/**
* Helper function for `adjustMapping`.
* @return {boolean}
*/
function isProblematicUnicodeLocation(code) {
// Using binary search to find a range start.
var i = 0, j = ProblematicCharRanges.length - 1;
while (i < j) {
var c = (i + j + 1) >> 1;
if (code < ProblematicCharRanges[c]) {
j = c - 1;
} else {
i = c;
}
}
// Even index means code in problematic range.
return !(i & 1);
}
/**
* Rebuilds the char code to glyph ID map by trying to replace the char codes
* with their unicode value. It also moves char codes that are in known
* problematic locations.
* Rebuilds the char code to glyph ID map by moving all char codes to the
* private use area. This is done to avoid issues with various problematic
* unicode areas where either a glyph won't be drawn or is deformed by a
* shaper.
* @return {Object} Two properties:
* 'toFontChar' - maps original char codes(the value that will be read
* from commands such as show text) to the char codes that will be used in the
* font that we build
* 'charCodeToGlyphId' - maps the new font char codes to glyph ids
*/
function adjustMapping(charCodeToGlyphId, properties, missingGlyphs) {
var toUnicode = properties.toUnicode;
var isSymbolic = !!(properties.flags & FontFlags.Symbolic);
var isIdentityUnicode =
properties.toUnicode instanceof IdentityToUnicodeMap;
function adjustMapping(charCodeToGlyphId, hasGlyph, newGlyphZeroId) {
var newMap = Object.create(null);
var toFontChar = [];
var usedFontCharCodes = [];
var nextAvailableFontCharCode = PRIVATE_USE_OFFSET_START;
var privateUseAreaIndex = 0;
var nextAvailableFontCharCode = PRIVATE_USE_AREAS[privateUseAreaIndex][0];
var privateUseOffetEnd = PRIVATE_USE_AREAS[privateUseAreaIndex][1];
for (var originalCharCode in charCodeToGlyphId) {
originalCharCode |= 0;
var glyphId = charCodeToGlyphId[originalCharCode];
// For missing glyphs don't create the mappings so the glyph isn't
// drawn.
if (missingGlyphs[glyphId]) {
if (!hasGlyph(glyphId)) {
continue;
}
var fontCharCode = originalCharCode;
// First try to map the value to a unicode position if a non identity map
// was created.
var hasUnicodeValue = false;
if (!isIdentityUnicode && toUnicode.has(originalCharCode)) {
hasUnicodeValue = true;
var unicode = toUnicode.get(fontCharCode);
// TODO: Try to map ligatures to the correct spot.
if (unicode.length === 1) {
fontCharCode = unicode.charCodeAt(0);
}
}
// Try to move control characters, special characters and already mapped
// characters to the private use area since they will not be drawn by
// canvas if left in their current position. Also, move characters if the
// font was symbolic and there is only an identity unicode map since the
// characters probably aren't in the correct position (fixes an issue
// with firefox and thuluthfont).
if ((usedFontCharCodes[fontCharCode] !== undefined ||
isProblematicUnicodeLocation(fontCharCode) ||
(isSymbolic && !hasUnicodeValue))) {
// Loop to try and find a free spot in the private use area.
do {
if (nextAvailableFontCharCode > PRIVATE_USE_OFFSET_END) {
if (nextAvailableFontCharCode > privateUseOffetEnd) {
privateUseAreaIndex++;
if (privateUseAreaIndex >= PRIVATE_USE_AREAS.length) {
warn('Ran out of space in font private use area.');
break;
}
fontCharCode = nextAvailableFontCharCode++;
if (SKIP_PRIVATE_USE_RANGE_F000_TO_F01F && fontCharCode === 0xF000) {
fontCharCode = 0xF020;
nextAvailableFontCharCode = fontCharCode + 1;
nextAvailableFontCharCode = PRIVATE_USE_AREAS[privateUseAreaIndex][0];
privateUseOffetEnd = PRIVATE_USE_AREAS[privateUseAreaIndex][1];
}
} while (usedFontCharCodes[fontCharCode] !== undefined);
var fontCharCode = nextAvailableFontCharCode++;
if (glyphId === 0) {
glyphId = newGlyphZeroId;
}
newMap[fontCharCode] = glyphId;
toFontChar[originalCharCode] = fontCharCode;
usedFontCharCodes[fontCharCode] = true;
}
return {
toFontChar,
@ -1076,6 +1006,11 @@ var Font = (function FontClosure() {
'Unicode ranges Bits > 123 are reserved for internal usage');
}
}
if (lastCharIndex > 0xFFFF) {
// OS2 only supports a 16 bit int. The spec says if supplementary
// characters are used the field should just be set to 0xFFFF.
lastCharIndex = 0xFFFF;
}
} else {
// TODO
firstCharIndex = 0;
@ -1863,14 +1798,14 @@ var Font = (function FontClosure() {
data[offset + 1] = (value >> 1) & 0xFF;
};
}
// The first glyph is duplicated.
var numGlyphsOut = dupFirstEntry ? numGlyphs + 1 : numGlyphs;
var locaData = loca.data;
var locaDataSize = itemSize * (1 + numGlyphs);
// is loca.data too short or long?
if (locaData.length !== locaDataSize) {
var locaDataSize = itemSize * (1 + numGlyphsOut);
// Resize loca table to account for duplicated glyph.
locaData = new Uint8Array(locaDataSize);
locaData.set(loca.data.subarray(0, locaDataSize));
loca.data = locaData;
}
// removing the invalid glyphs
var oldGlyfData = glyf.data;
var oldGlyfDataLength = oldGlyfData.length;
@ -1880,10 +1815,7 @@ var Font = (function FontClosure() {
var missingGlyphs = Object.create(null);
itemEncode(locaData, 0, writeOffset);
var i, j;
// When called with dupFirstEntry the number of glyphs has already been
// increased but there isn't data yet for the duplicated glyph.
var locaCount = dupFirstEntry ? numGlyphs - 1 : numGlyphs;
for (i = 0, j = itemSize; i < locaCount; i++, j += itemSize) {
for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
var endOffset = itemDecode(locaData, j);
// The spec says the offsets should be in ascending order, however
// some fonts use the offset of 0 to mark a glyph as missing.
@ -1921,11 +1853,14 @@ var Font = (function FontClosure() {
// to have single glyph with one point
var simpleGlyph = new Uint8Array(
[0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 49, 0]);
for (i = 0, j = itemSize; i < numGlyphs; i++, j += itemSize) {
for (i = 0, j = itemSize; i < numGlyphsOut; i++, j += itemSize) {
itemEncode(locaData, j, simpleGlyph.length);
}
glyf.data = simpleGlyph;
} else if (dupFirstEntry) {
// Browsers will not display a glyph at position 0. Typically glyph 0
// is notdef, but a number of fonts put a valid glyph there so it must
// be duplicated and appended.
var firstEntryLength = itemDecode(locaData, itemSize);
if (newGlyfData.length > firstEntryLength + writeOffset) {
glyf.data = newGlyfData.subarray(0, firstEntryLength + writeOffset);
@ -2382,7 +2317,15 @@ var Font = (function FontClosure() {
font.pos = (font.start || 0) + tables['maxp'].offset;
var version = font.getInt32();
var numGlyphs = font.getUint16();
const numGlyphs = font.getUint16();
// Glyph 0 is duplicated and appended.
let numGlyphsOut = numGlyphs + 1;
let dupFirstEntry = true;
if (numGlyphsOut > 0xFFFF) {
dupFirstEntry = false;
numGlyphsOut = numGlyphs;
warn('Not enough space in glyfs to duplicate first glyph.');
}
var maxFunctionDefs = 0;
var maxSizeOfInstructions = 0;
if (version >= 0x00010000 && tables['maxp'].length >= 22) {
@ -2399,15 +2342,8 @@ var Font = (function FontClosure() {
maxSizeOfInstructions = font.getUint16();
}
var dupFirstEntry = false;
if (properties.type === 'CIDFontType2' && properties.toUnicode &&
properties.toUnicode.get(0) > '\u0000') {
// oracle's defect (see 3427), duplicating first entry
dupFirstEntry = true;
numGlyphs++;
tables['maxp'].data[4] = numGlyphs >> 8;
tables['maxp'].data[5] = numGlyphs & 255;
}
tables['maxp'].data[4] = numGlyphsOut >> 8;
tables['maxp'].data[5] = numGlyphsOut & 255;
var hintsValid = sanitizeTTPrograms(tables['fpgm'], tables['prep'],
tables['cvt '], maxFunctionDefs);
@ -2419,7 +2355,7 @@ var Font = (function FontClosure() {
// Ensure the hmtx table contains the advance width and
// sidebearings information for numGlyphs in the maxp table
sanitizeMetrics(font, tables['hhea'], tables['hmtx'], numGlyphs);
sanitizeMetrics(font, tables['hhea'], tables['hmtx'], numGlyphsOut);
if (!tables['head']) {
throw new FormatError('Required "head" table is not found');
@ -2472,12 +2408,15 @@ var Font = (function FontClosure() {
// The 'post' table has glyphs names.
if (tables['post']) {
var valid = readPostScriptTable(tables['post'], properties, numGlyphs);
if (!valid) {
tables['post'] = null;
}
readPostScriptTable(tables['post'], properties, numGlyphs);
}
// The original 'post' table is not needed, replace it.
tables['post'] = {
tag: 'post',
data: createPostTable(properties),
};
var charCodeToGlyphId = [], charCode;
// Helper function to try to skip mapping of empty glyphs.
@ -2504,12 +2443,6 @@ var Font = (function FontClosure() {
charCodeToGlyphId[charCode] = glyphId;
}
});
if (dupFirstEntry && (isCidToGidMapEmpty || !charCodeToGlyphId[0])) {
// We don't duplicate the first entry in the `charCodeToGlyphId` map
// if the font has a `CIDToGIDMap` which has already mapped the first
// entry to a non-zero `glyphId` (fixes issue7544.pdf).
charCodeToGlyphId[0] = numGlyphs - 1;
}
} else {
// Most of the following logic in this code branch is based on the
// 9.6.6.4 of the PDF spec.
@ -2620,13 +2553,21 @@ var Font = (function FontClosure() {
charCodeToGlyphId[0] = 0;
}
// Typically glyph 0 is duplicated and the mapping must be updated, but if
// there isn't enough room to duplicate, the glyph id is left the same. In
// this case, glyph 0 may not work correctly, but that is better than
// having the whole font fail.
let glyphZeroId = numGlyphsOut - 1;
if (!dupFirstEntry) {
glyphZeroId = 0;
}
// Converting glyphs and ids into font's cmap table
var newMapping = adjustMapping(charCodeToGlyphId, properties,
missingGlyphs);
var newMapping = adjustMapping(charCodeToGlyphId, hasGlyph, glyphZeroId);
this.toFontChar = newMapping.toFontChar;
tables['cmap'] = {
tag: 'cmap',
data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphs),
data: createCmapTable(newMapping.charCodeToGlyphId, numGlyphsOut),
};
if (!tables['OS/2'] || !validateOS2Table(tables['OS/2'])) {
@ -2637,14 +2578,6 @@ var Font = (function FontClosure() {
};
}
// Rewrite the 'post' table if needed
if (!tables['post']) {
tables['post'] = {
tag: 'post',
data: createPostTable(properties),
};
}
if (!isTrueType) {
try {
// Trying to repair CFF file
@ -2652,6 +2585,7 @@ var Font = (function FontClosure() {
var parser = new CFFParser(cffFile, properties,
SEAC_ANALYSIS_ENABLED);
cff = parser.parse();
cff.duplicateFirstGlyph();
var compiler = new CFFCompiler(cff);
tables['CFF '].data = compiler.compile();
} catch (e) {
@ -2688,8 +2622,16 @@ var Font = (function FontClosure() {
adjustToUnicode(properties, properties.builtInEncoding);
}
// Type 1 fonts have a notdef inserted at the beginning, so glyph 0
// becomes glyph 1. In a CFF font glyph 0 is appended to the end of the
// char strings.
let glyphZeroId = 1;
if (font instanceof CFFFont) {
glyphZeroId = font.numGlyphs - 1;
}
var mapping = font.getGlyphMapping(properties);
var newMapping = adjustMapping(mapping, properties, Object.create(null));
var newMapping = adjustMapping(mapping, font.hasGlyphId.bind(font),
glyphZeroId);
this.toFontChar = newMapping.toFontChar;
var numGlyphs = font.numGlyphs;
@ -2927,12 +2869,14 @@ var Font = (function FontClosure() {
var seac = this.seacMap[charcode];
fontCharCode = seac.baseFontCharCode;
accent = {
fontChar: String.fromCharCode(seac.accentFontCharCode),
fontChar: String.fromCodePoint(seac.accentFontCharCode),
offset: seac.accentOffset,
};
}
var fontChar = String.fromCharCode(fontCharCode);
var fontChar = typeof fontCharCode === 'number' ?
String.fromCodePoint(fontCharCode) :
'';
var glyph = this.glyphCache[charcode];
if (!glyph ||
@ -3283,6 +3227,18 @@ var Type1Font = (function Type1FontClosure() {
return type1FontGlyphMapping(properties, builtInEncoding, glyphNames);
},
hasGlyphId: function Type1Font_hasGlyphID(id) {
if (id < 0 || id >= this.numGlyphs) {
return false;
}
if (id === 0) {
// notdef is always defined.
return true;
}
var glyph = this.charstrings[id - 1];
return glyph.charstring.length > 0;
},
getSeacs: function Type1Font_getSeacs(charstrings) {
var i, ii;
var seacMap = [];
@ -3382,14 +3338,7 @@ var Type1Font = (function Type1FontClosure() {
var charStringsIndex = new CFFIndex();
charStringsIndex.add([0x8B, 0x0E]); // .notdef
for (i = 0; i < count; i++) {
var glyph = glyphs[i];
// If the CharString outline is empty, replace it with .notdef to
// prevent OTS from rejecting the font (fixes bug1252420.pdf).
if (glyph.length === 0) {
charStringsIndex.add([0x8B, 0x0E]); // .notdef
continue;
}
charStringsIndex.add(glyph);
charStringsIndex.add(glyphs[i]);
}
cff.charStrings = charStringsIndex;
@ -3448,6 +3397,7 @@ var CFFFont = (function CFFFontClosure() {
var parser = new CFFParser(file, properties, SEAC_ANALYSIS_ENABLED);
this.cff = parser.parse();
this.cff.duplicateFirstGlyph();
var compiler = new CFFCompiler(this.cff);
this.seacs = this.cff.seacs;
try {
@ -3498,37 +3448,20 @@ var CFFFont = (function CFFFontClosure() {
charCodeToGlyphId = type1FontGlyphMapping(properties, encoding, charsets);
return charCodeToGlyphId;
},
hasGlyphId: function CFFFont_hasGlyphID(id) {
return this.cff.hasGlyphId(id);
},
};
return CFFFont;
})();
// Workaround for seac on Windows.
(function checkSeacSupport() {
if (typeof navigator !== 'undefined' && /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
(function checkChromeWindows() {
if (typeof navigator !== 'undefined' &&
/Windows.*Chrome/.test(navigator.userAgent)) {
SKIP_PRIVATE_USE_RANGE_F000_TO_F01F = true;
}
})();
export {
SEAC_ANALYSIS_ENABLED,
PRIVATE_USE_OFFSET_START,
PRIVATE_USE_OFFSET_END,
ErrorFont,
Font,
FontFlags,
ToUnicodeMap,
IdentityToUnicodeMap,
ProblematicCharRanges,
getFontType,
};

View File

@ -165,6 +165,24 @@ const hasDOM = typeof window === 'object' && typeof document === 'object';
globalScope.WeakMap = require('core-js/fn/weak-map');
})();
// Provides support for String.codePointAt in legacy browsers.
// Support: IE11.
(function checkStringCodePointAt() {
if (String.codePointAt) {
return;
}
String.codePointAt = require('core-js/fn/string/code-point-at');
})();
// Provides support for String.fromCodePoint in legacy browsers.
// Support: IE11.
(function checkStringFromCodePoint() {
if (String.fromCodePoint) {
return;
}
String.fromCodePoint = require('core-js/fn/string/from-code-point');
})();
} // End of !PDFJSDev.test('CHROME')
// Provides support for Object.values in legacy browsers.

View File

@ -114,6 +114,7 @@
!issue5686.pdf
!issue3928.pdf
!clippath.pdf
!issue8795_reduced.pdf
!close-path-bug.pdf
!issue6019.pdf
!issue6621.pdf
@ -281,6 +282,7 @@
!issue5475.pdf
!annotation-border-styles.pdf
!IdentityToUnicodeMap_charCodeOf.pdf
!PDFJS-9279-reduced.pdf
!issue5481.pdf
!issue5567.pdf
!issue5701.pdf
@ -303,6 +305,7 @@
!issue7014.pdf
!issue8187.pdf
!annotation-link-text-popup.pdf
!issue9278.pdf
!annotation-text-without-popup.pdf
!annotation-underline.pdf
!annotation-strikeout.pdf

Binary file not shown.

View File

@ -0,0 +1 @@
https://web.archive.org/web/20171116124714/https://www.unicode.org/charts/PDF/U11A50.pdf

File diff suppressed because one or more lines are too long

BIN
test/pdfs/issue9278.pdf Normal file

Binary file not shown.

View File

@ -122,6 +122,12 @@
"lastPage": 1,
"type": "eq"
},
{ "id": "issue8795_reduced",
"file": "pdfs/issue8795_reduced.pdf",
"md5": "3ce58fa4aff351d46c42e0677d582099",
"rounds": 1,
"type": "eq"
},
{ "id": "issue2391-1",
"file": "pdfs/issue2391-1.pdf",
"md5": "25ae9cb959612e7b343b55da63af2716",
@ -2436,6 +2442,12 @@
"link": false,
"type": "text"
},
{ "id": "issue9278",
"file": "pdfs/issue9278.pdf",
"md5": "9819c3a5715c1a46ea5a6740f9ead3da",
"rounds": 1,
"type": "eq"
},
{ "id": "bug894572",
"file": "pdfs/bug894572.pdf",
"md5": "e54a6b0451939f685ed37e3d46e16158",
@ -3460,6 +3472,15 @@
"rounds": 1,
"type": "eq"
},
{ "id": "bug1425312",
"file": "pdfs/bug1425312.pdf",
"md5": "5b1e7d3e4ba7792fab2b69d1836df5a9",
"link": true,
"rounds": 1,
"firstPage": 2,
"lastPage": 2,
"type": "eq"
},
{ "id": "issue3438",
"file": "pdfs/issue3438.pdf",
"md5": "5aa3340b0920b65a377f697587668f89",
@ -3699,6 +3720,12 @@
"type": "eq",
"about": "Image mask in higher resolution than the image itself"
},
{ "id": "PDFJS-9279-reduced",
"file": "pdfs/PDFJS-9279-reduced.pdf",
"md5": "a562a25596e9fe571ac6fb5b9f561974",
"rounds": 1,
"type": "eq"
},
{ "id": "issue4436",
"file": "pdfs/issue4436r.pdf",
"md5": "4e43d692d213f56674fcac92110c7364",

View File

@ -14,7 +14,7 @@
*/
import {
CFFCompiler, CFFParser, CFFStrings
CFFCompiler, CFFFDSelect, CFFParser, CFFStrings
} from '../../src/core/cff_parser';
import { SEAC_ANALYSIS_ENABLED } from '../../src/core/fonts';
import { Stream } from '../../src/core/stream';
@ -311,7 +311,7 @@ describe('CFFParser', function() {
var fdSelect = parser.parseFDSelect(0, 2);
expect(fdSelect.fdSelect).toEqual([0, 1]);
expect(fdSelect.raw).toEqual(bytes);
expect(fdSelect.format).toEqual(0);
});
it('parses fdselect format 3', function() {
@ -327,7 +327,7 @@ describe('CFFParser', function() {
var fdSelect = parser.parseFDSelect(0, 4);
expect(fdSelect.fdSelect).toEqual([9, 9, 0xa, 0xa]);
expect(fdSelect.raw).toEqual(bytes);
expect(fdSelect.format).toEqual(3);
});
it('parses invalid fdselect format 3 (bug 1146106)', function() {
@ -343,8 +343,7 @@ describe('CFFParser', function() {
var fdSelect = parser.parseFDSelect(0, 4);
expect(fdSelect.fdSelect).toEqual([9, 9, 0xa, 0xa]);
bytes[3] = bytes[4] = 0x00; // The adjusted first range, first gid.
expect(fdSelect.raw).toEqual(bytes);
expect(fdSelect.format).toEqual(3);
});
// TODO fdArray
@ -400,5 +399,52 @@ describe('CFFCompiler', function() {
expect(names[0].length).toEqual(127);
});
it('compiles fdselect format 0', function() {
var fdSelect = new CFFFDSelect(0, [3, 2, 1]);
var c = new CFFCompiler();
var out = c.compileFDSelect(fdSelect);
expect(out).toEqual([
0, // format
3, // gid: 0 fd 3
2, // gid: 1 fd 3
1, // gid: 2 fd 3
]);
});
it('compiles fdselect format 3', function() {
var fdSelect = new CFFFDSelect(3, [0, 0, 1, 1]);
var c = new CFFCompiler();
var out = c.compileFDSelect(fdSelect);
expect(out).toEqual([
3, // format
0, // nRanges (high)
2, // nRanges (low)
0, // range struct 0 - first (high)
0, // range struct 0 - first (low)
0, // range struct 0 - fd
0, // range struct 0 - first (high)
2, // range struct 0 - first (low)
1, // range struct 0 - fd
0, // sentinel (high)
4, // sentinel (low)
]);
});
it('compiles fdselect format 3, single range', function() {
var fdSelect = new CFFFDSelect(3, [0, 0]);
var c = new CFFCompiler();
var out = c.compileFDSelect(fdSelect);
expect(out).toEqual([
3, // format
0, // nRanges (high)
1, // nRanges (low)
0, // range struct 0 - first (high)
0, // range struct 0 - first (low)
0, // range struct 0 - fd
0, // sentinel (high)
2, // sentinel (low)
]);
});
// TODO a lot more compiler tests
});

View File

@ -18,7 +18,6 @@
"dom_utils_spec.js",
"encodings_spec.js",
"evaluator_spec.js",
"fonts_spec.js",
"function_spec.js",
"message_handler_spec.js",
"metadata_spec.js",

View File

@ -1,91 +0,0 @@
/* Copyright 2017 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
PRIVATE_USE_OFFSET_END, PRIVATE_USE_OFFSET_START, ProblematicCharRanges
} from '../../src/core/fonts';
/**
* Used to validate the entries in `ProblematicCharRanges`, and to ensure that
* its total number of characters does not exceed the PUA (Private Use Area)
* length.
* @returns {Object} An object with {number} `numChars`, {number} `puaLength`,
* and {number} `percentage` parameters.
*/
var checkProblematicCharRanges = function checkProblematicCharRanges() {
function printRange(limits) {
return '[' + limits.lower.toString('16').toUpperCase() + ', ' +
limits.upper.toString('16').toUpperCase() + ')';
}
var numRanges = ProblematicCharRanges.length;
if (numRanges % 2 !== 0) {
throw new Error('Char ranges must contain an even number of elements.');
}
var prevLimits, numChars = 0;
for (var i = 0; i < numRanges; i += 2) {
var limits = {
lower: ProblematicCharRanges[i],
upper: ProblematicCharRanges[i + 1],
};
if (!Number.isInteger(limits.lower) || !Number.isInteger(limits.upper)) {
throw new Error('Range endpoints must be integers: ' +
printRange(limits));
}
if (limits.lower < 0 || limits.upper < 0) {
throw new Error('Range endpoints must be non-negative: ' +
printRange(limits));
}
var range = limits.upper - limits.lower;
if (range < 1) {
throw new Error('Range must contain at least one element: ' +
printRange(limits));
}
if (prevLimits) {
if (limits.lower < prevLimits.lower) {
throw new Error('Ranges must be sorted in ascending order: ' +
printRange(limits) + ', ' + printRange(prevLimits));
}
if (limits.lower < prevLimits.upper) {
throw new Error('Ranges must not overlap: ' +
printRange(limits) + ', ' + printRange(prevLimits));
}
}
prevLimits = {
lower: limits.lower,
upper: limits.upper,
};
// The current range is OK.
numChars += range;
}
var puaLength = (PRIVATE_USE_OFFSET_END + 1) - PRIVATE_USE_OFFSET_START;
if (numChars > puaLength) {
throw new Error('Total number of chars must not exceed the PUA length.');
}
return {
numChars,
puaLength,
percentage: 100 * (numChars / puaLength),
};
};
describe('Fonts', function() {
it('checkProblematicCharRanges', function() {
var EXPECTED_PERCENTAGE = 100;
var result = checkProblematicCharRanges();
expect(result.percentage).toBeLessThan(EXPECTED_PERCENTAGE);
});
});

View File

@ -60,7 +60,6 @@ function initializePDFJS(callback) {
'pdfjs-test/unit/dom_utils_spec',
'pdfjs-test/unit/encodings_spec',
'pdfjs-test/unit/evaluator_spec',
'pdfjs-test/unit/fonts_spec',
'pdfjs-test/unit/function_spec',
'pdfjs-test/unit/message_handler_spec',
'pdfjs-test/unit/metadata_spec',