Skip to content

Commit 4b26552

Browse files
authored
feat: add type3 glyph font test support (#401)
* feat: add type3 glyph font test support * fix: architectural compliance, separate the type3 glyph fonts processing from rendering, use standard canvas text rendering pipeline for glyph, tested with /test/pdf/misc/i389_type3_glyph.pdf * test: add test/_test_type3glyph.cjs * issue fixed: #389, #377, #332
1 parent b198d60 commit 4b26552

File tree

7 files changed

+600
-53
lines changed

7 files changed

+600
-53
lines changed

base/core/evaluator.js

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -506,19 +506,84 @@ var PartialEvaluator = (function PartialEvaluatorClosure() {
506506
var fontResources = font.get('Resources') || resources;
507507
var charProcKeys = Object.keys(charProcs);
508508
var charProcOperatorList = {};
509+
510+
info(`Processing Type3 font: ${fontName}, found ${charProcKeys.length} CharProcs`);
511+
512+
// Create a mapping from character code to glyph name
513+
var charProcMapping = {};
514+
var encoding = font.get('Encoding');
515+
516+
if (encoding) {
517+
info(`Type3 font has encoding: ${encoding.name || 'custom'}`);
518+
var differences = encoding.get('Differences');
519+
var baseEncoding = encoding.get('BaseEncoding');
520+
521+
// Process Differences array if it exists
522+
if (differences) {
523+
info(`Processing Differences array of length ${differences.length}`);
524+
var currentCode = 0;
525+
for (var i = 0; i < differences.length; i++) {
526+
var entry = differences[i];
527+
if (typeof entry === 'number') {
528+
currentCode = entry;
529+
info(`Setting current code to ${currentCode}`);
530+
} else {
531+
// Check the type of entry to debug what's happening
532+
var entryType = typeof entry;
533+
var entryValue;
534+
535+
// Ensure we always get a string name (not an object)
536+
if (entryType === 'object' && entry.name) {
537+
entryValue = entry.name;
538+
} else if (entryType === 'object') {
539+
entryValue = JSON.stringify(entry);
540+
info(`Warning: Non-name object in Differences array: ${entryValue}`);
541+
} else {
542+
entryValue = entry.toString();
543+
}
544+
545+
// info(`Entry type: ${entryType}, value: ${entryValue}`);
546+
547+
charProcMapping[currentCode] = entryValue;
548+
// info(`Mapped code ${currentCode} to glyph '${entryValue}'`);
549+
currentCode++;
550+
}
551+
}
552+
}
553+
// Use BaseEncoding if available
554+
if (baseEncoding && baseEncoding.name) {
555+
info(`Using BaseEncoding: ${baseEncoding.name}`);
556+
var baseEncodingMap = Encodings[baseEncoding.name];
557+
if (baseEncodingMap) {
558+
for (var code = 0; code < 256; code++) {
559+
if (!charProcMapping[code] && baseEncodingMap[code]) {
560+
charProcMapping[code] = baseEncodingMap[code];
561+
// info(`Mapped code ${code} to glyph '${baseEncodingMap[code]}' from BaseEncoding`);
562+
}
563+
}
564+
}
565+
}
566+
}
567+
568+
// Store the mapping in the font object for text extraction
569+
font.translated.charProcMapping = charProcMapping;
570+
// info(`Final charProcMapping has ${Object.keys(charProcMapping).length} entries`);
571+
509572
for (var i = 0, n = charProcKeys.length; i < n; ++i) {
510573
var key = charProcKeys[i];
511574
var glyphStream = charProcs[key];
512575
var operatorList = this.getOperatorList(glyphStream, fontResources);
513576
charProcOperatorList[key] = operatorList.getIR();
577+
// info(`Processed CharProc for glyph '${key}'`);
514578
if (!parentOperatorList) {
515579
continue;
516580
}
517581
// Add the dependencies to the parent operator list so they are
518582
// resolved before sub operator list is executed synchronously.
519-
parentOperatorList.addDependencies(charProcOperatorList.dependencies);
583+
parentOperatorList.addDependencies(operatorList.dependencies);
520584
}
521585
font.translated.charProcOperatorList = charProcOperatorList;
586+
font.translated.charProcMapping = charProcMapping;
522587
font.loaded = true;
523588
} else {
524589
font.loaded = true;

base/core/fonts.js

Lines changed: 70 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2185,11 +2185,40 @@ var Font = (function FontClosure() {
21852185
this.cmap = properties.cmap;
21862186

21872187
this.fontMatrix = properties.fontMatrix;
2188-
if (properties.type == 'Type3') {
2189-
this.encoding = properties.baseEncoding;
2190-
return;
2188+
if (properties.type == 'Type3') {
2189+
this.encoding = properties.baseEncoding;
2190+
this.disableFontFace = true;
2191+
this.loadedName = this.loadedName || 'Type3Font';
2192+
2193+
// Add ability to map Type3 font glyphs to Unicode characters
2194+
if (properties.toUnicode) {
2195+
this.toUnicode = properties.toUnicode;
2196+
} else {
2197+
// Create a basic toUnicode map for common glyph names
2198+
const toUnicode = {};
2199+
const encoding = properties.baseEncoding || [];
2200+
for (let i = 0; i < encoding.length; i++) {
2201+
const glyphName = encoding[i];
2202+
if (glyphName && GlyphsUnicode[glyphName]) {
2203+
toUnicode[i] = String.fromCharCode(GlyphsUnicode[glyphName]);
2204+
}
2205+
}
2206+
2207+
// If there are differences, apply them too
2208+
if (properties.differences && properties.differences.length) {
2209+
for (let i = 0; i < 256; i++) {
2210+
if (properties.differences[i]) {
2211+
const glyphName = properties.differences[i];
2212+
if (typeof glyphName === 'string' && GlyphsUnicode[glyphName]) {
2213+
toUnicode[i] = String.fromCharCode(GlyphsUnicode[glyphName]);
2214+
}
2215+
}
2216+
}
2217+
}
2218+
this.toUnicode = toUnicode;
2219+
}
2220+
return;
21912221
}
2192-
21932222
// Trying to fix encoding using glyph CIDSystemInfo.
21942223
this.loadCidToUnicode(properties);
21952224
this.cidEncoding = properties.cidEncoding;
@@ -4494,7 +4523,43 @@ var Font = (function FontClosure() {
44944523
case 'Type3':
44954524
var glyphName = this.differences[charcode] || this.encoding[charcode];
44964525
operatorList = this.charProcOperatorList[glyphName];
4497-
fontCharCode = charcode;
4526+
4527+
// For text extraction, map the glyph name to Unicode if possible
4528+
if (glyphName) {
4529+
fontCharCode = GlyphsUnicode[glyphName] || charcode;
4530+
4531+
// Handle common symbolic glyphs
4532+
if (fontCharCode === charcode && typeof glyphName === 'string') {
4533+
// Special handling for specific glyphs
4534+
if (glyphName.startsWith('uni')) {
4535+
// Handle uniXXXX format
4536+
const hex = glyphName.substring(3);
4537+
if (/^[0-9A-F]{4,6}$/i.test(hex)) {
4538+
fontCharCode = parseInt(hex, 16);
4539+
}
4540+
}
4541+
4542+
// Check if it's a common symbol
4543+
const commonSymbols = {
4544+
'bullet': 0x2022,
4545+
'checkbox': 0x2610,
4546+
'checkmark': 0x2713,
4547+
'circle': 0x25CB,
4548+
'square': 0x25A1,
4549+
'triangle': 0x25B2,
4550+
'triangledown': 0x25BC,
4551+
'triangleleft': 0x25C0,
4552+
'triangleright': 0x25B6,
4553+
'star': 0x2605
4554+
};
4555+
4556+
if (commonSymbols[glyphName.toLowerCase()]) {
4557+
fontCharCode = commonSymbols[glyphName.toLowerCase()];
4558+
}
4559+
}
4560+
} else {
4561+
fontCharCode = charcode;
4562+
}
44984563
break;
44994564
case 'TrueType':
45004565
if (this.useToFontChar) {

base/display/canvas.js

Lines changed: 82 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -903,8 +903,10 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
903903
this.current.fontSize = size;
904904

905905
if (fontObj.coded) {
906-
warn('Unsupported Type3 font (custom Glyph) - ' + fontRefName);
907-
return; // we don't need ctx.font for Type3 fonts
906+
warn('Found Type3 font (custom Glyph) - ' + fontRefName + ', trying to decode'); // MQZ 8/23 added Type3 glyph font support
907+
// MQZ. 08/24/2025 need to set up the font context for glyph based text processing
908+
this.ctx.setFont(fontObj);
909+
return; // we don't need ctx.font for Type3 fonts
908910
}
909911

910912
var name = fontObj.loadedName || 'sans-serif';
@@ -1053,13 +1055,36 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
10531055
var glyphsLength = glyphs.length;
10541056
var textLayer = this.textLayer;
10551057
var geom;
1056-
var textSelection = textLayer && !skipTextSelection ? true : false;
1058+
1059+
// Always use textSelection for Type3 fonts
1060+
var textSelection = textLayer && (font.coded || !skipTextSelection) ? true : false;
1061+
var type3Text = "";
1062+
10571063
var canvasWidth = 0.0;
10581064
var vertical = font.vertical;
10591065
var defaultVMetrics = font.defaultVMetrics;
10601066

1067+
info(`showText called with ${glyphsLength} glyphs, font type: ${font.coded ? 'Type3' : font.type || 'Unknown'}, textSelection: ${textSelection}`);
1068+
10611069
// Type3 fonts - each glyph is a "mini-PDF"
10621070
if (font.coded) {
1071+
info(`Processing Type3 font with ${glyphsLength} glyphs`);
1072+
1073+
// For Type3 fonts, collect unicode characters or character codes
1074+
for (var i = 0; i < glyphsLength; ++i) {
1075+
var glyph = glyphs[i];
1076+
if (glyph !== null) {
1077+
// Use unicode value if available, otherwise use fontChar
1078+
if (glyph.unicode) {
1079+
type3Text += glyph.unicode;
1080+
} else if (glyph.fontChar) {
1081+
type3Text += String.fromCharCode(glyph.fontChar);
1082+
}
1083+
}
1084+
}
1085+
info(`Type3 text: ${type3Text}`);
1086+
1087+
// If we have collected text, store it for later use in appendText
10631088
ctx.save();
10641089
ctx.transform.apply(ctx, current.textMatrix);
10651090
ctx.translate(current.x, current.y);
@@ -1070,18 +1095,22 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
10701095
this.save();
10711096
ctx.scale(1, -1);
10721097
geom = this.createTextGeometry();
1098+
// Add the Type3 text to the geometry object so it can be added to the output
1099+
geom.type3Text = type3Text;
1100+
geom.fontSize = fontSize;
10731101
this.restore();
10741102
}
10751103
for (var i = 0; i < glyphsLength; ++i) {
1076-
10771104
var glyph = glyphs[i];
10781105
if (glyph === null) {
10791106
// word break
1107+
info(`Type3 word break at glyph ${i}`);
10801108
this.ctx.translate(wordSpacing, 0);
10811109
current.x += wordSpacing * textHScale;
10821110
continue;
10831111
}
10841112

1113+
//info(`Processing Type3 glyph ${i}: ${glyph.unicode || glyph.fontChar}`);
10851114
this.processingType3 = glyph;
10861115
this.save();
10871116
ctx.scale(fontSize, fontSize);
@@ -1093,24 +1122,46 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
10931122
var width = (transformed[0] * fontSize + charSpacing) *
10941123
current.fontDirection;
10951124

1125+
//info(`Type3 glyph width: ${width}`);
10961126
ctx.translate(width, 0);
10971127
current.x += width * textHScale;
10981128

10991129
canvasWidth += width;
11001130
}
1131+
// Render Type3 text within the transformation context
1132+
if (type3Text) {
1133+
info(`render Type3 text: '${type3Text}', disableFontFace: ${font.disableFontFace}`);
1134+
var curFontSize = fontSize;
1135+
switch (current.textRenderingMode) {
1136+
case TextRenderingMode.FILL:
1137+
ctx.fillText(type3Text, 0, 0, canvasWidth, curFontSize);
1138+
break;
1139+
case TextRenderingMode.STROKE:
1140+
ctx.strokeText(type3Text, 0, 0, canvasWidth, curFontSize);
1141+
break;
1142+
case TextRenderingMode.FILL_STROKE:
1143+
ctx.fillText(type3Text, 0, 0, canvasWidth, curFontSize);
1144+
break;
1145+
case TextRenderingMode.INVISIBLE:
1146+
case TextRenderingMode.ADD_TO_PATH:
1147+
break;
1148+
default: // other unsupported rendering modes
1149+
}
1150+
}
1151+
11011152
ctx.restore();
11021153
this.processingType3 = null;
11031154
} else {
11041155
ctx.save();
11051156

1106-
//MQZ Dec.04.2013 handles leading word spacing
1107-
var tx = 0;
1108-
if (wordSpacing !== 0) {
1109-
var firstGlyph = glyphs.filter(g => g && ('fontChar' in g || 'unicode' in g))[0];
1110-
if (firstGlyph && (firstGlyph.fontChar === ' ' || firstGlyph.unicode === ' ')) {
1157+
//MQZ Dec.04.2013 handles leading word spacing
1158+
var tx = 0;
1159+
if (wordSpacing !== 0) {
1160+
var firstGlyph = glyphs.filter(g => g && ('fontChar' in g || 'unicode' in g))[0];
1161+
if (firstGlyph && (firstGlyph.fontChar === ' ' || firstGlyph.unicode === ' ')) {
11111162
tx = wordSpacing * fontSize * textHScale;
1112-
}
1113-
}
1163+
}
1164+
}
11141165

11151166
current.x += tx
11161167
this.applyTextTransforms();
@@ -1135,8 +1186,8 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
11351186

11361187
ctx.lineWidth = lineWidth;
11371188

1138-
//MQZ. Feb.20.2013. Disable character based painting, make it a string
1139-
var str = "";
1189+
//MQZ. Feb.20.2013. Disable character based painting, make it a string
1190+
var str = "";
11401191

11411192
var x = 0;
11421193
for (var i = 0; i < glyphsLength; ++i) {
@@ -1188,7 +1239,7 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
11881239

11891240
//MQZ. Feb.20.2013. Disable character based painting, make it a string
11901241
// this.paintChar(character, scaledX, scaledY);
1191-
str += glyph.unicode || character;
1242+
str += glyph.unicode || character;
11921243
if (accent) {
11931244
scaledAccentX = scaledX + accent.offset.x / fontSizeScale;
11941245
scaledAccentY = scaledY - accent.offset.y / fontSizeScale;
@@ -1218,35 +1269,28 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
12181269
// info(nodeUtil.inspect(glyphs));
12191270
// }
12201271

1221-
if (str && !font.disableFontFace) {
1222-
var curFontSize = fontSize * scale * textHScale + 3;
1223-
switch (current.textRenderingMode) {
1224-
case TextRenderingMode.FILL:
1225-
ctx.fillText(str, 0, 0, canvasWidth, curFontSize);
1226-
break;
1227-
case TextRenderingMode.STROKE:
1228-
ctx.strokeText(str, 0, 0, canvasWidth, curFontSize);
1229-
break;
1230-
case TextRenderingMode.FILL_STROKE:
1231-
ctx.fillText(str, 0, 0, canvasWidth, curFontSize);
1232-
break;
1233-
case TextRenderingMode.INVISIBLE:
1234-
case TextRenderingMode.ADD_TO_PATH:
1235-
break;
1236-
default: // other unsupported rendering modes
1237-
}
1238-
}
12391272

12401273
ctx.restore();
12411274
}
12421275

1243-
if (textSelection) {
1244-
geom.canvasWidth = canvasWidth;
1245-
if (vertical) {
1246-
var VERTICAL_TEXT_ROTATION = Math.PI / 2;
1247-
geom.angle += VERTICAL_TEXT_ROTATION;
1276+
// Text rendering for regular fonts (Type3 fonts are handled in their own context above)
1277+
if (str && !font.disableFontFace && !font.coded) {
1278+
var curFontSize = fontSize * scale * textHScale + 3;
1279+
switch (current.textRenderingMode) {
1280+
case TextRenderingMode.FILL:
1281+
ctx.fillText(str, 0, 0, canvasWidth, curFontSize);
1282+
break;
1283+
case TextRenderingMode.STROKE:
1284+
ctx.strokeText(str, 0, 0, canvasWidth, curFontSize);
1285+
break;
1286+
case TextRenderingMode.FILL_STROKE:
1287+
ctx.fillText(str, 0, 0, canvasWidth, curFontSize);
1288+
break;
1289+
case TextRenderingMode.INVISIBLE:
1290+
case TextRenderingMode.ADD_TO_PATH:
1291+
break;
1292+
default: // other unsupported rendering modes
12481293
}
1249-
this.textLayer.appendText(geom);
12501294
}
12511295

12521296
return canvasWidth;
@@ -1334,7 +1378,6 @@ var CanvasGraphics = (function CanvasGraphicsClosure() {
13341378
var VERTICAL_TEXT_ROTATION = Math.PI / 2;
13351379
geom.angle += VERTICAL_TEXT_ROTATION;
13361380
}
1337-
this.textLayer.appendText(geom);
13381381
}
13391382
},
13401383
nextLineShowText: function CanvasGraphics_nextLineShowText(text) {

0 commit comments

Comments
 (0)