Skip to content

Commit

Permalink
Add a parser to get font data from the default appearance
Browse files Browse the repository at this point in the history
 - pdfium & poppler use a special parser too to get these info.
  • Loading branch information
calixteman committed Jan 7, 2021
1 parent ed3758f commit 0d23011
Show file tree
Hide file tree
Showing 3 changed files with 189 additions and 26 deletions.
48 changes: 27 additions & 21 deletions src/core/annotation.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ import {
import { Catalog, FileSpec, ObjectLoader } from "./obj.js";
import { collectActions, getInheritableProperty } from "./core_utils.js";
import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js";
import {
dumpDefaultAppearance,
parseDefaultAppearance,
} from "./defaultappearance.js";
import { ColorSpace } from "./colorspace.js";
import { OperatorList } from "./operator_list.js";
import { StringStream } from "./stream.js";
Expand Down Expand Up @@ -991,6 +995,10 @@ class WidgetAnnotation extends Annotation {
data.defaultAppearance = isString(defaultAppearance)
? defaultAppearance
: "";
this._defaultAppearanceData = parseDefaultAppearance(
data.defaultAppearance
);

const fieldType = getInheritableProperty({ dict, key: "FT" });
data.fieldType = isName(fieldType) ? fieldType.name : null;

Expand Down Expand Up @@ -1284,10 +1292,9 @@ class WidgetAnnotation extends Annotation {
this.data.defaultAppearance = "/Helvetica 0 Tf 0 g";
}

const fontInfo = await this._getFontData(evaluator, task);
const [font, fontName] = fontInfo;
const fontSize = this._computeFontSize(...fontInfo, totalHeight);
this._fontName = fontName;
const font = await this._getFontData(evaluator, task);
const fontSize = this._computeFontSize(font, totalHeight);
this._fontName = this._defaultAppearanceData.fontName.name;

let descent = font.descent;
if (isNaN(descent)) {
Expand Down Expand Up @@ -1352,42 +1359,41 @@ class WidgetAnnotation extends Annotation {
async _getFontData(evaluator, task) {
const operatorList = new OperatorList();
const initialState = {
fontSize: 0,
font: null,
fontName: null,
clone() {
return this;
},
};

await evaluator.getOperatorList({
stream: new StringStream(this.data.defaultAppearance),
task,
resources: this._fieldResources.mergedResources,
const { fontName, fontSize } = this._defaultAppearanceData;
await evaluator.handleSetFont(
this._fieldResources.mergedResources,
[fontName, fontSize],
null,
operatorList,
task,
initialState,
});
null
);

return [initialState.font, initialState.fontName, initialState.fontSize];
return initialState.font;
}

_computeFontSize(font, fontName, fontSize, height) {
_computeFontSize(font, height) {
let fontSize = this._defaultAppearanceData.fontSize;
if (fontSize === null || fontSize === 0) {
const { color, fontName } = this._defaultAppearanceData;
const em = font.charsToGlyphs("M")[0].width / 1000;
// According to https://en.wikipedia.org/wiki/Em_(typography)
// an average cap height should be 70% of 1em
const capHeight = 0.7 * em;
// 1.5 * capHeight * fontSize seems to be a good value for lineHeight
fontSize = Math.max(1, Math.floor(height / (1.5 * capHeight)));

let fontRegex = new RegExp(`/${fontName}\\s+[0-9.]+\\s+Tf`);
if (this.data.defaultAppearance.search(fontRegex) === -1) {
// The font size is missing
fontRegex = new RegExp(`/${fontName}\\s+Tf`);
}
this.data.defaultAppearance = this.data.defaultAppearance.replace(
fontRegex,
`/${fontName} ${fontSize} Tf`
this.data.defaultAppearance = dumpDefaultAppearance(
fontSize,
fontName,
color
);
}
return fontSize;
Expand Down
161 changes: 161 additions & 0 deletions src/core/defaultappearance.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/* Copyright 2020 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 { isName, Name } from "./primitives.js";
import { escapePDFName } from "./core_utils.js";

// Parse DA to extract font and color informations
function parseDefaultAppearance(str) {
const result = {
fontSize: null,
fontName: null,
color: null,
};

if (!str) {
return result;
}

const stack = [];

for (const token of str.split(/\s+/)) {
if (!token) {
continue;
}
switch (token.charAt(0)) {
case "/":
// we've a name
const name = token
.slice(1)
.replace(/#([0-9a-fA-F]{2})/, (match, n) =>
String.fromCharCode(n.parseInt(16))
);
stack.push(Name.get(name));
break;
case "-":
case "0":
case "1":
case "2":
case "3":
case "4":
case "5":
case "6":
case "7":
case "8":
case "9":
case ".":
// we've a number
stack.push(Number.parseFloat(token));
break;
case "T":
if (token !== "Tf") {
break;
}
// font op
// two last elements in stack must be a Name and number
if (stack.length < 2) {
break;
}
const fontSize = stack.pop();
const fontName = stack.pop();

if (typeof fontSize !== "number") {
break;
}
if (!isName(fontName)) {
break;
}
result.fontSize = fontSize;
result.fontName = fontName;
break;
case "g":
// gray op
if (stack.length < 1) {
break;
}
const gray = stack.pop();
if (typeof gray !== "number") {
break;
}
result.color = [gray];
break;
case "r":
if (token !== "rg") {
break;
}
// rgb op
if (stack.length < 3) {
break;
}
const r = stack.pop();
const g = stack.pop();
const b = stack.pop();
if (
typeof r !== "number" ||
typeof g !== "number" ||
typeof b !== "number"
) {
break;
}
result.color = [r, g, b];
break;
case "k":
// cmyk op
const c = stack.pop();
const m = stack.pop();
const y = stack.pop();
const k = stack.pop();
if (
typeof c !== "number" ||
typeof m !== "number" ||
typeof y !== "number" ||
typeof k !== "number"
) {
break;
}
result.color = [c, m, y, k];
break;
}
}

return result;
}

function dumpDefaultAppearance(fontSize, fontName, color) {
fontSize = fontSize || 0;
fontName = isName(fontName) ? fontName.name : fontName;

if (color) {
let colorCmd = "";
switch (color.length) {
case 1:
colorCmd = "g";
break;
case 2:
colorCmd = "rg";
break;
case 4:
colorCmd = "k";
break;
}
color = ` ${color.join(" ")} ${colorCmd}`;
} else {
color = "";
}

return `/${escapePDFName(fontName)} ${fontSize} Tf${color}`;
}

export { dumpDefaultAppearance, parseDefaultAppearance };
6 changes: 1 addition & 5 deletions src/core/evaluator.js
Original file line number Diff line number Diff line change
Expand Up @@ -797,12 +797,10 @@ class PartialEvaluator {
fallbackFontDict = null
) {
// TODO(mack): Not needed?
var fontName,
fontSize = 0;
var fontName;
if (fontArgs) {
fontArgs = fontArgs.slice();
fontName = fontArgs[0].name;
fontSize = fontArgs[1];
}

return this.loadFont(fontName, fontRef, resources, fallbackFontDict)
Expand Down Expand Up @@ -835,8 +833,6 @@ class PartialEvaluator {
})
.then(translated => {
state.font = translated.font;
state.fontSize = fontSize;
state.fontName = fontName;
translated.send(this.handler);
return translated.loadedName;
});
Expand Down

0 comments on commit 0d23011

Please sign in to comment.