Merge pull request #2606 from mduan/issue1512

For TTFs, implement section 9.6.6.4 from the PDF specs
This commit is contained in:
Yury Delendik 2013-01-30 11:12:27 -08:00
commit 8504a3b6b3
6 changed files with 348 additions and 298 deletions

View File

@ -2571,7 +2571,7 @@ var Font = (function FontClosure() {
return ranges;
};
function createCMapTable(glyphs, deltas) {
function createCmapTable(glyphs, deltas) {
var ranges = getRanges(glyphs);
var numTables = 1;
@ -2847,6 +2847,37 @@ var Font = (function FontClosure() {
return nameTable;
}
// Normalize the charcodes in the cmap table into unicode values
// that will work with the (3, 1) cmap table we will write out.
function cmapCharcodeToUnicode(charcode, symbolic, platformId, encodingId) {
var unicode;
if (symbolic) {
// These codes will be shifted into the range
// SYMBOLIC_FONT_GLYPH_OFFSET to (SYMBOLIC_FONT_GLYPH_OFFSET + 0xFF)
// so that they are not in the control character range that could
// be displayed as spaces by browsers.
if (platformId === 3 && encodingId === 0 ||
platformId === 1 && encodingId === 0) {
unicode = SYMBOLIC_FONT_GLYPH_OFFSET | (charcode & 0xFF);
}
} else {
if (platformId === 3 && encodingId === 1) {
// A (3, 1) table is alredy unicode (Microsoft Unicode format)
unicode = charcode;
} else if (platformId === 1 && encodingId === 0) {
// TODO(mack): Should apply the changes to convert the
// MacRomanEncoding to Mac OS Roman encoding in 9.6.6.4
// table 115 of the pdf spec
var glyphName = Encodings.MacRomanEncoding[charcode];
if (glyphName) {
unicode = GlyphsUnicode[glyphName];
}
}
}
return unicode;
}
Font.prototype = {
name: null,
font: null,
@ -2929,88 +2960,127 @@ var Font = (function FontClosure() {
properties.baseEncoding = encoding;
}
function readCMapTable(cmap, font) {
/**
* Read the appropriate subtable from the cmap according to 9.6.6.4 from
* PDF spec
*/
function readCmapTable(cmap, font, hasEncoding, isSymbolicFont) {
var start = (font.start ? font.start : 0) + cmap.offset;
font.pos = start;
var version = int16(font.getBytes(2));
var numRecords = int16(font.getBytes(2));
var numTables = int16(font.getBytes(2));
var records = [];
for (var i = 0; i < numRecords; i++) {
records.push({
platformID: int16(font.getBytes(2)),
encodingID: int16(font.getBytes(2)),
offset: int32(font.getBytes(4))
});
var potentialTable;
var foundPreferredTable;
// There's an order of preference in terms of which cmap subtable we
// want to use. So scan through them to find our preferred table.
for (var i = 0; i < numTables; i++) {
var platformId = int16(font.getBytes(2));
var encodingId = int16(font.getBytes(2));
var offset = int32(font.getBytes(4));
var useTable = false;
var canBreak = false;
// The following block implements the following from the spec:
//
// When the font has no Encoding entry, or the font descriptors
// Symbolic flag is set (in which case the Encoding entry
// is ignored), this shall occur:
// - If the font contains a (3, 0) subtable, the range of
// - Otherwise, the (1, 0) subtable will be used.
// Otherwise, if the font does have an encoding:
// - Use the (3, 1) cmap subtable
// - Otherwise, use the (1, 0) subtable if present
//
// The following diverges slightly from the above spec in order
// to handle the case that hasEncoding and isSymbolicFont are both
// true. In this, based on the ordering of the rules in the spec,
// my interpretation is that we should be acting as if the font is
// symbolic.
//
// However, in this case, the test pdf 'preistabelle.pdf'
// is interpreting this case as a non-symbolic font. In this case
// though, 'presitabelle.pdf' does contain a (3, 1) table and does
// not contain a (3, 0) table which indicates it is non-symbolic.
//
// Thus, I am using this heurisitic of looking at which table is
// found to truly determine whether or not the font is symbolic.
// That is, if the specific symbolic/non-symbolic font specific
// tables (3, 0) or (3, 1) is found, that information is used for
// deciding if the font is symbolic or not.
//
// TODO(mack): This section needs some more thought on whether the
// heuristic is good enough. For now, it passes all the regression
// tests.
if (isSymbolicFont && platformId === 3 && encodingId === 0) {
useTable = true;
canBreak = true;
foundPreferredTable = true;
} else if (hasEncoding && platformId === 3 && encodingId === 1) {
useTable = true;
canBreak = true;
foundPreferredTable = true;
// Update the isSymbolicFont based on this heuristic
isSymbolicFont = false;
} else if (platformId === 1 && encodingId === 0 &&
!foundPreferredTable) {
useTable = true;
foundPreferredTable = true;
} else if (!potentialTable) {
// We will use an arbitrary table if we cannot find a preferred
// table
useTable = true;
}
// Check that table are sorted by platformID then encodingID,
records.sort(function fontReadCMapTableSort(a, b) {
return ((a.platformID << 16) + a.encodingID) -
((b.platformID << 16) + b.encodingID);
});
var tables = [records[0]];
for (var i = 1; i < numRecords; i++) {
// The sanitizer will drop the font if 2 tables have the same
// platformID and the same encodingID, this will be correct for
// most cases but if the font has been made for Mac it could
// exist a few platformID: 1, encodingID: 0 but with a different
// language field and that's correct. But the sanitizer does not
// seem to support this case.
var current = records[i];
var previous = records[i - 1];
if (((current.platformID << 16) + current.encodingID) <=
((previous.platformID << 16) + previous.encodingID))
continue;
tables.push(current);
if (useTable) {
potentialTable = {
platformId: platformId,
encodingId: encodingId,
offset: offset,
isSymbolicFont: isSymbolicFont
};
}
if (canBreak) {
break;
}
}
var missing = numRecords - tables.length;
if (missing) {
numRecords = tables.length;
var data = string16(version) + string16(numRecords);
for (var i = 0; i < numRecords; i++) {
var table = tables[i];
data += string16(table.platformID) +
string16(table.encodingID) +
string32(table.offset);
if (!potentialTable) {
error('Could not find a cmap table');
return;
}
for (var i = 0, ii = data.length; i < ii; i++)
cmap.data[i] = data.charCodeAt(i);
if (!foundPreferredTable) {
warn('Did not find a cmap of suitable format. Interpreting (' +
potentialTable.platformId + ', ' + potentialTable.encodingId +
') as (3, 1) table');
potentialTable.platformId = 3;
potentialTable.encodingId = 1;
}
for (var i = 0; i < numRecords; i++) {
var table = tables[i];
font.pos = start + table.offset;
font.pos = start + potentialTable.offset;
var format = int16(font.getBytes(2));
var length = int16(font.getBytes(2));
var language = int16(font.getBytes(2));
if (format == 0) {
// Characters below 0x20 are controls characters that are hardcoded
// into the platform so if some characters in the font are assigned
// under this limit they will not be displayed so let's rewrite the
// CMap.
var glyphs = [];
var ids = [];
var hasShortCmap = false;
var mappings = [];
// TODO(mack): refactor this cmap subtable reading logic out
if (format === 0) {
for (var j = 0; j < 256; j++) {
var index = font.getByte();
if (index) {
glyphs.push({ unicode: j, code: j });
ids.push(index);
if (!index) {
continue;
}
mappings.push({
charcode: j,
glyphId: index
});
}
return {
glyphs: glyphs,
ids: ids,
hasShortCmap: true
};
} else if (format == 4) {
hasShortCmap = true;
} else if (format === 4) {
// re-creating the table in format 4 since the encoding
// might be changed
var segCount = (int16(font.getBytes(2)) >> 1);
@ -3044,10 +3114,9 @@ var Font = (function FontClosure() {
}
var offsets = [];
for (var j = 0; j < offsetsCount; j++)
for (var j = 0; j < offsetsCount; j++) {
offsets.push(int16(font.getBytes(2)));
var glyphs = [], ids = [];
}
for (segIndex = 0; segIndex < segCount; segIndex++) {
var segment = segments[segIndex];
@ -3055,24 +3124,22 @@ var Font = (function FontClosure() {
var delta = segment.delta, offsetIndex = segment.offsetIndex;
for (var j = start; j <= end; j++) {
if (j == 0xFFFF)
if (j == 0xFFFF) {
continue;
}
var glyphCode = offsetIndex < 0 ? j :
var glyphId = offsetIndex < 0 ? j :
offsets[offsetIndex + j - start];
glyphCode = (glyphCode + delta) & 0xFFFF;
if (glyphCode == 0)
glyphId = (glyphId + delta) & 0xFFFF;
if (glyphId === 0) {
continue;
glyphs.push({ unicode: j, code: j });
ids.push(glyphCode);
}
mappings.push({
charcode: j,
glyphId: glyphId
});
}
}
return {
glyphs: glyphs,
ids: ids
};
} else if (format == 6) {
// Format 6 is a 2-bytes dense mapping, which means the font data
// lives glue together even if they are pretty far in the unicode
@ -3085,20 +3152,25 @@ var Font = (function FontClosure() {
var glyphs = [];
var ids = [];
for (var j = 0; j < entryCount; j++) {
var glyphCode = int16(font.getBytes(2));
var code = firstCode + j;
var glyphId = int16(font.getBytes(2));
var charcode = firstCode + j;
glyphs.push({ unicode: code, code: code });
ids.push(glyphCode);
mappings.push({
charcode: charcode,
glyphId: glyphId
});
}
} else {
error('cmap table has unsupported format: ' + format);
}
return {
glyphs: glyphs,
ids: ids
platformId: potentialTable.platformId,
encodingId: potentialTable.encodingId,
isSymbolicFont: potentialTable.isSymbolicFont,
mappings: mappings,
hasShortCmap: hasShortCmap
};
}
}
error('Unsupported cmap table format');
};
function sanitizeMetrics(font, header, metrics, numGlyphs) {
@ -3699,175 +3771,131 @@ var Font = (function FontClosure() {
ids.push(i);
}
} else {
var cmapTable = readCMapTable(cmap, font);
this.useToFontChar = true;
// Most of the following logic in this code branch is based on the
// 9.6.6.4 of the PDF spec.
glyphs = cmapTable.glyphs;
ids = cmapTable.ids;
// TODO(mack):
// We are using this.hasEncoding to mean that the encoding is either
// MacRomanEncoding or WinAnsiEncoding (following spec in 9.6.6.4),
// but this.hasEncoding is currently true for any encodings on the
// Encodings object (e.g. MacExpertEncoding). So should consider using
// better check for this.
var cmapTable = readCmapTable(cmap, font, this.hasEncoding,
this.isSymbolicFont);
var hasShortCmap = !!cmapTable.hasShortCmap;
// TODO(mack): If the (3, 0) cmap table used, then the font is
// symbolic. The range of charcodes in the cmap table should be
// one of the following:
// -> 0x0000 - 0x00FF
// -> 0xF000 - 0xF0FF
// -> 0xF100 - 0xF1FF
// -> 0xF200 - 0xF2FF
// If it is not, we should change not consider this a symbolic font
this.isSymbolicFont = cmapTable.isSymbolicFont;
var cmapPlatformId = cmapTable.platformId;
var cmapEncodingId = cmapTable.encodingId;
var cmapMappings = cmapTable.mappings;
var cmapMappingsLength = cmapMappings.length;
var glyphs = [];
var ids = [];
for (var i = 0; i < cmapMappingsLength; ++i) {
var cmapMapping = cmapMappings[i];
var charcode = cmapMapping.charcode;
var unicode = cmapCharcodeToUnicode(charcode, this.isSymbolicFont,
cmapPlatformId, cmapEncodingId);
if (!unicode) {
// TODO(mack): gotta check if skipping mappings where we cannot find
// a unicode is the correct behaviour
continue;
}
glyphs.push({
code: charcode,
unicode: unicode
});
ids.push(cmapMapping.glyphId);
}
var hasShortCmap = cmapTable.hasShortCmap;
var toFontChar = this.toFontChar;
if (hasShortCmap && ids.length == numGlyphs) {
// Fixes the short cmap tables -- some generators use incorrect
// glyph id.
for (var i = 0, ii = ids.length; i < ii; i++)
for (var i = 0, ii = ids.length; i < ii; i++) {
ids[i] = i;
}
}
// Rewrite the whole toFontChar dictionary with a new one using the
// information from the mappings in the cmap table.
var newToFontChar = [];
if (this.isSymbolicFont) {
for (var i = 0, ii = glyphs.length; i < ii; i++) {
var glyph = glyphs[i];
// For (3, 0) cmap tables:
// The charcode key being stored in toFontChar is the lower byte
// of the two-byte charcodes of the cmap table since according to
// the spec: 'each byte from the string shall be prepended with the
// high byte of the range [of charcodes in the cmap table], to form
// a two-byte character, which shall be used to select the
// associated glyph description from the subtable'.
//
// For (1, 0) cmap tables:
// 'single bytes from the string shall be used to look up the
// associated glyph descriptions from the subtable'. This means
// charcodes in the cmap will be single bytes, so no-op since
// glyph.code & 0xFF === glyph.code
newToFontChar[glyph.code & 0xFF] = glyph.unicode;
}
} else {
var unusedUnicode = CMAP_GLYPH_OFFSET;
var glyphNames = properties.glyphNames || [];
var encoding = properties.baseEncoding;
var differences = properties.differences;
if (toFontChar && toFontChar.length > 0) {
// checking if cmap is just identity map
var isIdentity = true;
for (var i = 0, ii = glyphs.length; i < ii; i++) {
if (glyphs[i].unicode != i + 1) {
isIdentity = false;
break;
}
}
// if it is, replacing with meaningful toUnicode values
if (isIdentity && !this.isSymbolicFont) {
var usedUnicodes = [], unassignedUnicodeItems = [];
for (var i = 0, ii = glyphs.length; i < ii; i++) {
var unicode = toFontChar[i + 1];
if (!unicode || typeof unicode !== 'number' ||
unicode in usedUnicodes) {
unassignedUnicodeItems.push(i);
// TODO(mack): check if it is necessary to shift control characters
// for non-symbolic fonts so that browsers dont't render them using
// space characters
var glyphCodeMapping = cmapTable.glyphCodeMapping;
for (var charcode = 0; charcode < encoding.length; ++charcode) {
if (!encoding.hasOwnProperty(charcode)) {
continue;
}
glyphs[i].unicode = unicode;
usedUnicodes[unicode] = true;
}
for (var j = 0, jj = unassignedUnicodeItems.length; j < jj; j++) {
var i = unassignedUnicodeItems[j];
while (unusedUnicode in usedUnicodes)
unusedUnicode++;
var cid = i + 1;
// override only if unicode mapping is not specified
if (!(cid in toFontChar))
toFontChar[cid] = unusedUnicode;
glyphs[i].unicode = unusedUnicode++;
}
this.useToFontChar = true;
}
// Since the cmap table that we will be writing out is a (3, 1)
// unicode table, in this section we will rewrites the charcodes
// in the pdf into unicodes
var glyphName = encoding[charcode];
// A nonsymbolic font should not have a Differences array, but
// if it does have one, we should still use it
if (charcode in differences) {
glyphName = differences[charcode];
}
// remove glyph references outside range of avaialable glyphs or empty
var glyphsRemoved = 0;
for (var i = ids.length - 1; i >= 0; i--) {
if (ids[i] < numGlyphs &&
(!emptyGlyphIds[ids[i]] || this.isSymbolicFont))
continue;
ids.splice(i, 1);
glyphs.splice(i, 1);
glyphsRemoved++;
}
// checking if it's a "true" symbolic font
if (this.isSymbolicFont) {
var minUnicode = 0xFFFF, maxUnicode = 0;
for (var i = 0, ii = glyphs.length; i < ii; i++) {
var unicode = glyphs[i].unicode;
minUnicode = Math.min(minUnicode, unicode);
maxUnicode = Math.max(maxUnicode, unicode);
}
// high byte must be the same for min and max unicodes
if ((maxUnicode & 0xFF00) != (minUnicode & 0xFF00))
this.isSymbolicFont = false;
}
// heuristics: if removed more than 5 glyphs encoding WinAnsiEncoding
// does not set properly (broken PDFs have about 100 removed glyphs)
if (glyphsRemoved > 5) {
warn('Switching TrueType encoding to MacRomanEncoding for ' +
this.name + ' font');
encoding = Encodings.MacRomanEncoding;
}
if (hasShortCmap && this.hasEncoding && !this.isSymbolicFont) {
// Re-encode short map encoding to unicode -- that simplifies the
// resolution of MacRoman encoded glyphs logic for TrueType fonts:
// copying all characters to private use area, all mapping all known
// glyphs to the unicodes. The glyphs and ids arrays will grow.
var usedUnicodes = [];
for (var i = 0, ii = glyphs.length; i < ii; i++) {
var code = glyphs[i].unicode;
var gid = ids[i];
glyphs[i].unicode += CMAP_GLYPH_OFFSET;
toFontChar[code] = glyphs[i].unicode;
var glyphName = glyphNames[gid] || encoding[code];
if (glyphName in GlyphsUnicode) {
var unicode = GlyphsUnicode[glyphName];
if (unicode in usedUnicodes)
continue;
usedUnicodes[unicode] = true;
glyphs.push({
unicode: unicode,
code: glyphs[i].code
});
ids.push(gid);
toFontChar[code] = unicode;
}
}
this.useToFontChar = true;
} else if (!this.isSymbolicFont && (this.hasEncoding ||
properties.glyphNames || differences.length > 0)) {
// Re-encode cmap encoding to unicode, based on the 'post' table data
// diffrence array or base encoding
var reverseMap = [];
for (var i = 0, ii = glyphs.length; i < ii; i++)
reverseMap[glyphs[i].unicode] = i;
var newGlyphUnicodes = [];
for (var i = 0, ii = glyphs.length; i < ii; i++) {
var code = glyphs[i].unicode;
var changeCode = false;
var gid = ids[i];
var glyphName = glyphNames[gid];
// Finally, any undefined entries in the table shall be filled
// using StandardEncoding
if (!glyphName) {
glyphName = differences[code] || encoding[code];
changeCode = true;
glyphName = Encodings.StandardEncoding[charcode];
}
if (glyphName in GlyphsUnicode) {
// TODO(mack): Handle the case that the glyph name cannot be
// mapped as specified, in which case the glyph name shall be
// looked up in the font program's 'post' table (if one is
// present) and the associated glyph id shall be used.
//
// For now, we're just using the '.notdef' glyph name in this
// case.
glyphName = glyphName || '.notdef';
var unicode = GlyphsUnicode[glyphName];
if (!unicode || reverseMap[unicode] === i)
continue; // unknown glyph name or in its own place
newGlyphUnicodes[i] = unicode;
if (changeCode)
toFontChar[code] = unicode;
delete reverseMap[code];
newToFontChar[charcode] = unicode;
}
}
for (var index in newGlyphUnicodes) {
if (newGlyphUnicodes.hasOwnProperty(index)) {
var unicode = newGlyphUnicodes[index];
if (reverseMap[unicode]) {
// avoiding assigning to the same unicode
glyphs[index].unicode = unusedUnicode++;
continue;
}
glyphs[index].unicode = unicode;
reverseMap[unicode] = index;
}
}
this.useToFontChar = true;
}
// Moving all symbolic font glyphs into 0xF000 - 0xF0FF range.
if (this.isSymbolicFont) {
for (var i = 0, ii = glyphs.length; i < ii; i++) {
var code = glyphs[i].unicode & 0xFF;
var fontCharCode = SYMBOLIC_FONT_GLYPH_OFFSET | code;
glyphs[i].unicode = toFontChar[code] = fontCharCode;
}
this.useToFontChar = true;
}
this.toFontChar = toFontChar = newToFontChar;
createGlyphNameMap(glyphs, ids, properties);
this.glyphNameMap = properties.glyphNameMap;
@ -3880,7 +3908,7 @@ var Font = (function FontClosure() {
}
// Converting glyphs and ids into font's cmap table
cmap.data = createCMapTable(glyphs, ids);
cmap.data = createCmapTable(glyphs, ids);
var unicodeIsEnabled = [];
for (var i = 0, ii = glyphs.length; i < ii; i++) {
unicodeIsEnabled[glyphs[i].unicode] = true;
@ -4024,7 +4052,7 @@ var Font = (function FontClosure() {
'OS/2': stringToArray(createOS2Table(properties, charstrings)),
// Character to glyphs mapping
'cmap': createCMapTable(charstrings.slice(),
'cmap': createCmapTable(charstrings.slice(),
('glyphIds' in font) ? font.glyphIds : null),
// Font header

View File

@ -10,7 +10,8 @@ describe('font_post', function() {
var font = new Font("font", new Stream(font2154), {
loadedName: 'font',
type: 'TrueType',
differences: []
differences: [],
baseEncoding: []
});
ttx(font.data, function(result) { output = result; });
runs(function() {

View File

@ -11,7 +11,8 @@ describe('font_post', function() {
var font = new Font("font", new Stream(font2109), {
loadedName: 'font',
type: 'CIDFontType2',
differences: []
differences: [],
baseEncoding: []
});
ttx(font.data, function(result) { output = result; });
runs(function() {
@ -26,7 +27,8 @@ describe('font_post', function() {
var font = new Font("font", new Stream(font2189), {
loadedName: 'font',
type: 'TrueType',
differences: []
differences: [],
baseEncoding: []
});
ttx(font.data, function(result) { output = result; });
runs(function() {
@ -41,7 +43,8 @@ describe('font_post', function() {
var font = new Font("font", new Stream(font2374), {
loadedName: 'font',
type: 'TrueType',
differences: []
differences: [],
baseEncoding: []
});
ttx(font.data, function(result) { output = result; });
runs(function() {

View File

@ -0,0 +1 @@
https://dok.dkb.de/pdf/aenderung_l_sepa.pdf

View File

@ -0,0 +1 @@
http://www.aeinstein.org/organizations/org/FDTD.pdf

View File

@ -52,6 +52,22 @@
"rounds": 1,
"type": "eq"
},
{ "id": "german-umlaut",
"file": "pdfs/german-umlaut.pdf",
"md5": "331de67c1397702315970a871d8a369b",
"link": true,
"pageLimit": 1,
"rounds": 1,
"type": "eq"
},
{ "id": "issue1512",
"file": "pdfs/issue1512.pdf",
"md5": "41a19fe03d522346ee3baa732403fca4",
"link": true,
"pageLimit": 1,
"rounds": 1,
"type": "eq"
},
{ "id": "pdfspec-load",
"file": "pdfs/pdf.pdf",
"md5": "dbdb23c939d2be09b43126c3c56060c7",