diff --git a/src/core/annotation.js b/src/core/annotation.js index 0c2280b49926e..c66305d9e3ada 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -35,6 +35,10 @@ import { } from "../shared/util.js"; import { Catalog, FileSpec, ObjectLoader } from "./obj.js"; import { collectActions, getInheritableProperty } from "./core_utils.js"; +import { + createDefaultAppearance, + parseDefaultAppearance, +} from "./default_appearance.js"; import { Dict, isDict, isName, isRef, isStream, Name } from "./primitives.js"; import { ColorSpace } from "./colorspace.js"; import { OperatorList } from "./operator_list.js"; @@ -993,6 +997,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; @@ -1288,12 +1296,14 @@ class WidgetAnnotation extends Annotation { // Doing so prevents exceptions and allows saving/printing // the file as expected. this.data.defaultAppearance = "/Helvetica 0 Tf 0 g"; + this._defaultAppearanceData = parseDefaultAppearance( + this.data.defaultAppearance + ); } - 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)) { @@ -1364,27 +1374,30 @@ 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], + /* fontRef = */ null, operatorList, + task, initialState, - }); + /* fallbackFontDict = */ null + ); - return [initialState.font, initialState.fontName, initialState.fontSize]; + return initialState.font; } - _computeFontSize(font, fontName, fontSize, height) { - if (fontSize === null || fontSize === 0) { + _computeFontSize(font, height) { + let fontSize = this._defaultAppearanceData.fontSize; + if (!fontSize) { + const { fontColor, fontName } = this._defaultAppearanceData; let capHeight; if (font.capHeight) { capHeight = font.capHeight; @@ -1403,15 +1416,11 @@ class WidgetAnnotation extends Annotation { // 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 = createDefaultAppearance({ + fontSize, + fontName, + fontColor, + }); } return fontSize; } diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js new file mode 100644 index 0000000000000..2b7eba5e69be9 --- /dev/null +++ b/src/core/default_appearance.js @@ -0,0 +1,96 @@ +/* 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 { OPS, warn } from "../shared/util.js"; +import { ColorSpace } from "./colorspace.js"; +import { escapePDFName } from "./core_utils.js"; +import { EvaluatorPreprocessor } from "./evaluator.js"; +import { StringStream } from "./stream.js"; + +class DefaultAppearanceEvaluator extends EvaluatorPreprocessor { + constructor(str) { + super(new StringStream(str)); + } + + parse() { + const operation = { + fn: 0, + args: [], + }; + const result = { + fontSize: 0, + fontName: Name.get(""), + fontColor: new Uint8ClampedArray([0, 0, 0]) /* black */, + }; + + try { + while (this.read(operation)) { + if (this.stateManager.stateStack.length !== 0) { + // Don't get info in save/restore sections. + args.length = 0; + continue; + } + const { fn, args } = operation; + switch (fn | 0) { + case OPS.setFont: + const [fontName, fontSize] = args; + if (isName(fontName)) { + result.fontName = fontName; + } + if (typeof fontSize === "number" && fontSize > 0) { + result.fontSize = fontSize; + } + break; + case OPS.setFillRGBColor: + ColorSpace.singletons.rgb.getRgbItem(args, 0, result.fontColor, 0); + break; + case OPS.setFillGray: + ColorSpace.singletons.gray.getRgbItem(args, 0, result.fontColor, 0); + break; + case OPS.setFillColorSpace: + ColorSpace.singletons.cmyk.getRgbItem(args, 0, result.fontColor, 0); + break; + } + args.length = 0; + } + } catch (reason) { + warn(`parseDefaultAppearance - ignoring errors: "${reason}".`); + } + + return result; + } +} + +// Parse DA to extract font and color information. +function parseDefaultAppearance(str) { + return new DefaultAppearanceEvaluator(str).parse(); +} + +// Create default appearance string from some information. +function createDefaultAppearance({ fontSize, fontName, fontColor }) { + let colorCmd; + if (fontColor.every(c => c === 0)) { + colorCmd = "0 g"; + } else { + colorCmd = + Array.from(fontColor) + .map(c => (c / 255).toFixed(2)) + .join(" ") + " rg"; + } + return `/${escapePDFName(fontName.name)} ${fontSize} Tf ${colorCmd}`; +} + +export { createDefaultAppearance, parseDefaultAppearance }; diff --git a/src/core/evaluator.js b/src/core/evaluator.js index 5e794a66b6b9c..fb0c075aee499 100644 --- a/src/core/evaluator.js +++ b/src/core/evaluator.js @@ -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) @@ -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; }); @@ -3714,7 +3710,7 @@ class TranslatedFont { } class StateManager { - constructor(initialState) { + constructor(initialState = new EvalState()) { this.state = initialState; this.stateStack = []; } @@ -3985,7 +3981,7 @@ class EvaluatorPreprocessor { return shadow(this, "MAX_INVALID_PATH_OPS", 20); } - constructor(stream, xref, stateManager) { + constructor(stream, xref, stateManager = new StateManager()) { // TODO(mduan): pass array of knownCommands rather than this.opMap // dictionary this.parser = new Parser({ @@ -4126,4 +4122,4 @@ class EvaluatorPreprocessor { } } -export { PartialEvaluator }; +export { EvaluatorPreprocessor, PartialEvaluator }; diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index 18b4bc393d15f..222a1a0e9f8e2 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -1808,7 +1808,7 @@ describe("annotation", function () { }, done.fail) .then(appearance => { expect(appearance).toEqual( - "/Tx BMC q BT /Helv 11 Tf 1 0 0 1 0 0 Tm" + + "/Tx BMC q BT /Helv 11 Tf 0 g 1 0 0 1 0 0 Tm" + " 2.00 2.00 Td (test \\(print\\)) Tj ET Q EMC" ); done(); @@ -1848,7 +1848,7 @@ describe("annotation", function () { "\x30\x53\x30\x93\x30\x6b\x30\x61" + "\x30\x6f\x4e\x16\x75\x4c\x30\x6e"; expect(appearance).toEqual( - "/Tx BMC q BT /Goth 9 Tf 1 0 0 1 0 0 Tm" + + "/Tx BMC q BT /Goth 9 Tf 0 g 1 0 0 1 0 0 Tm" + ` 2.00 2.00 Td (${utf16String}) Tj ET Q EMC` ); done(); diff --git a/test/unit/clitests.json b/test/unit/clitests.json index d4766617c1763..1f6ed900ff5a0 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -16,6 +16,7 @@ "core_utils_spec.js", "crypto_spec.js", "custom_spec.js", + "default_appearance_spec.js", "display_svg_spec.js", "display_utils_spec.js", "document_spec.js", diff --git a/test/unit/default_appearance_spec.js b/test/unit/default_appearance_spec.js new file mode 100644 index 0000000000000..53ba81f5e0def --- /dev/null +++ b/test/unit/default_appearance_spec.js @@ -0,0 +1,55 @@ +/* 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 { + createDefaultAppearance, + parseDefaultAppearance, +} from "../../src/core/default_appearance.js"; +import { Name } from "../../src/core/primitives.js"; + +describe("Default appearance", function () { + describe("parseDefaultAppearance and createDefaultAppearance", function () { + it("should parse and create default appearance", function () { + const da = "/FontName 12 Tf 0.10 0.20 0.30 rg"; + const result = { + fontSize: 12, + fontName: Name.get("FontName"), + fontColor: new Uint8ClampedArray([26, 51, 76]), + }; + expect(parseDefaultAppearance(da)).toEqual(result); + expect(createDefaultAppearance(result)).toEqual(da); + + expect( + parseDefaultAppearance( + " 0.1 0.2 0.3 rg /FontName 12 Tf 0.3 0.2 0.1 rg /NameFont 13 Tf" + ) + ).toEqual({ + fontSize: 13, + fontName: Name.get("NameFont"), + fontColor: new Uint8ClampedArray([76, 51, 26]), + }); + }); + + it("should parse default appearance with save/restore", function () { + const da = + "0.10 0.20 0.30 rg /FontName 12 Tf q 0.30 0.20 0.10 rg /NameFont 13 Tf Q"; + expect(parseDefaultAppearance(da)).toEqual({ + fontSize: 12, + fontName: Name.get("FontName"), + fontColor: new Uint8ClampedArray([26, 51, 76]), + }); + }); + }); +}); diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index f12469d15d349..5d436d08a5014 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -60,6 +60,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/core_utils_spec.js", "pdfjs-test/unit/crypto_spec.js", "pdfjs-test/unit/custom_spec.js", + "pdfjs-test/unit/default_appearance_spec.js", "pdfjs-test/unit/display_svg_spec.js", "pdfjs-test/unit/display_utils_spec.js", "pdfjs-test/unit/document_spec.js",