mirror of
https://github.com/mozilla/pdf.js.git
synced 2025-04-25 01:28:06 +02:00
- For text fields * when printing, we generate a fake font which contains some widths computed thanks to an OffscreenCanvas and its method measureText. In order to avoid to have to layout the glyphs ourselves, we just render all of them in one call in the showText method in using the system sans-serif/monospace fonts. * when saving, we continue to create the appearance streams if the fonts contain the char but when a char is missing, we just set, in the AcroForm dict, the flag /NeedAppearances to true and remove the appearance stream. This way, we let the different readers handle the rendering of the strings. - For FreeText annotations * when printing, we use the same trick as for text fields. * there is no need to save an appearance since Acrobat is able to infer one from the Content entry.
355 lines
10 KiB
JavaScript
355 lines
10 KiB
JavaScript
/* 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 { Dict, Name } from "./primitives.js";
|
|
import {
|
|
escapePDFName,
|
|
getRotationMatrix,
|
|
numberToString,
|
|
stringToUTF16HexString,
|
|
} from "./core_utils.js";
|
|
import { LINE_DESCENT_FACTOR, LINE_FACTOR, OPS, warn } from "../shared/util.js";
|
|
import { ColorSpace } from "./colorspace.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: "",
|
|
fontColor: /* black = */ new Uint8ClampedArray(3),
|
|
};
|
|
|
|
try {
|
|
while (true) {
|
|
operation.args.length = 0; // Ensure that `args` it's always reset.
|
|
|
|
if (!this.read(operation)) {
|
|
break;
|
|
}
|
|
if (this.savedStatesDepth !== 0) {
|
|
continue; // Don't get info in save/restore sections.
|
|
}
|
|
const { fn, args } = operation;
|
|
|
|
switch (fn | 0) {
|
|
case OPS.setFont:
|
|
const [fontName, fontSize] = args;
|
|
if (fontName instanceof Name) {
|
|
result.fontName = fontName.name;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
} 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();
|
|
}
|
|
|
|
function getPdfColor(color, isFill) {
|
|
if (color[0] === color[1] && color[1] === color[2]) {
|
|
const gray = color[0] / 255;
|
|
return `${numberToString(gray)} ${isFill ? "g" : "G"}`;
|
|
}
|
|
return (
|
|
Array.from(color, c => numberToString(c / 255)).join(" ") +
|
|
` ${isFill ? "rg" : "RG"}`
|
|
);
|
|
}
|
|
|
|
// Create default appearance string from some information.
|
|
function createDefaultAppearance({ fontSize, fontName, fontColor }) {
|
|
return `/${escapePDFName(fontName)} ${fontSize} Tf ${getPdfColor(
|
|
fontColor,
|
|
/* isFill */ true
|
|
)}`;
|
|
}
|
|
|
|
class FakeUnicodeFont {
|
|
constructor(xref, fontFamily) {
|
|
this.xref = xref;
|
|
this.widths = null;
|
|
this.firstChar = Infinity;
|
|
this.lastChar = -Infinity;
|
|
this.fontFamily = fontFamily;
|
|
|
|
const canvas = new OffscreenCanvas(1, 1);
|
|
this.ctxMeasure = canvas.getContext("2d");
|
|
|
|
if (!FakeUnicodeFont._fontNameId) {
|
|
FakeUnicodeFont._fontNameId = 1;
|
|
}
|
|
this.fontName = Name.get(
|
|
`InvalidPDFjsFont_${fontFamily}_${FakeUnicodeFont._fontNameId++}`
|
|
);
|
|
}
|
|
|
|
get toUnicodeRef() {
|
|
if (!FakeUnicodeFont._toUnicodeRef) {
|
|
const toUnicode = `/CIDInit /ProcSet findresource begin
|
|
12 dict begin
|
|
begincmap
|
|
/CIDSystemInfo
|
|
<< /Registry (Adobe)
|
|
/Ordering (UCS) /Supplement 0 >> def
|
|
/CMapName /Adobe-Identity-UCS def
|
|
/CMapType 2 def
|
|
1 begincodespacerange
|
|
<0000> <FFFF>
|
|
endcodespacerange
|
|
1 beginbfrange
|
|
<0000> <FFFF> <0000>
|
|
endbfrange
|
|
endcmap CMapName currentdict /CMap defineresource pop end end`;
|
|
const toUnicodeStream = (FakeUnicodeFont.toUnicodeStream =
|
|
new StringStream(toUnicode));
|
|
const toUnicodeDict = new Dict(this.xref);
|
|
toUnicodeStream.dict = toUnicodeDict;
|
|
toUnicodeDict.set("Length", toUnicode.length);
|
|
FakeUnicodeFont._toUnicodeRef =
|
|
this.xref.getNewPersistentRef(toUnicodeStream);
|
|
}
|
|
|
|
return FakeUnicodeFont._toUnicodeRef;
|
|
}
|
|
|
|
get fontDescriptorRef() {
|
|
if (!FakeUnicodeFont._fontDescriptorRef) {
|
|
const fontDescriptor = new Dict(this.xref);
|
|
fontDescriptor.set("Type", Name.get("FontDescriptor"));
|
|
fontDescriptor.set("FontName", this.fontName);
|
|
fontDescriptor.set("FontFamily", "MyriadPro Regular");
|
|
fontDescriptor.set("FontBBox", [0, 0, 0, 0]);
|
|
fontDescriptor.set("FontStretch", Name.get("Normal"));
|
|
fontDescriptor.set("FontWeight", 400);
|
|
fontDescriptor.set("ItalicAngle", 0);
|
|
|
|
FakeUnicodeFont._fontDescriptorRef =
|
|
this.xref.getNewPersistentRef(fontDescriptor);
|
|
}
|
|
|
|
return FakeUnicodeFont._fontDescriptorRef;
|
|
}
|
|
|
|
get descendantFontRef() {
|
|
const descendantFont = new Dict(this.xref);
|
|
descendantFont.set("BaseFont", this.fontName);
|
|
descendantFont.set("Type", Name.get("Font"));
|
|
descendantFont.set("Subtype", Name.get("CIDFontType0"));
|
|
descendantFont.set("CIDToGIDMap", Name.get("Identity"));
|
|
descendantFont.set("FirstChar", this.firstChar);
|
|
descendantFont.set("LastChar", this.lastChar);
|
|
descendantFont.set("FontDescriptor", this.fontDescriptorRef);
|
|
descendantFont.set("DW", 1000);
|
|
|
|
const widths = [];
|
|
const chars = [...this.widths.entries()].sort();
|
|
let currentChar = null;
|
|
let currentWidths = null;
|
|
for (const [char, width] of chars) {
|
|
if (!currentChar) {
|
|
currentChar = char;
|
|
currentWidths = [width];
|
|
continue;
|
|
}
|
|
if (char === currentChar + currentWidths.length) {
|
|
currentWidths.push(width);
|
|
} else {
|
|
widths.push(currentChar, currentWidths);
|
|
currentChar = char;
|
|
currentWidths = [width];
|
|
}
|
|
}
|
|
|
|
if (currentChar) {
|
|
widths.push(currentChar, currentWidths);
|
|
}
|
|
|
|
descendantFont.set("W", widths);
|
|
|
|
const cidSystemInfo = new Dict(this.xref);
|
|
cidSystemInfo.set("Ordering", "Identity");
|
|
cidSystemInfo.set("Registry", "Adobe");
|
|
cidSystemInfo.set("Supplement", 0);
|
|
descendantFont.set("CIDSystemInfo", cidSystemInfo);
|
|
|
|
return this.xref.getNewPersistentRef(descendantFont);
|
|
}
|
|
|
|
get baseFontRef() {
|
|
const baseFont = new Dict(this.xref);
|
|
baseFont.set("BaseFont", this.fontName);
|
|
baseFont.set("Type", Name.get("Font"));
|
|
baseFont.set("Subtype", Name.get("Type0"));
|
|
baseFont.set("Encoding", Name.get("Identity-H"));
|
|
baseFont.set("DescendantFonts", [this.descendantFontRef]);
|
|
baseFont.set("ToUnicode", this.toUnicodeRef);
|
|
|
|
return this.xref.getNewPersistentRef(baseFont);
|
|
}
|
|
|
|
get resources() {
|
|
const resources = new Dict(this.xref);
|
|
const font = new Dict(this.xref);
|
|
font.set(this.fontName.name, this.baseFontRef);
|
|
resources.set("Font", font);
|
|
|
|
return resources;
|
|
}
|
|
|
|
_createContext() {
|
|
this.widths = new Map();
|
|
this.ctxMeasure.font = `1000px ${this.fontFamily}`;
|
|
|
|
return this.ctxMeasure;
|
|
}
|
|
|
|
createFontResources(text) {
|
|
const ctx = this._createContext();
|
|
for (const line of text.split(/\r\n?|\n/)) {
|
|
for (const char of line.split("")) {
|
|
const code = char.charCodeAt(0);
|
|
if (this.widths.has(code)) {
|
|
continue;
|
|
}
|
|
const metrics = ctx.measureText(char);
|
|
const width = Math.ceil(metrics.width);
|
|
this.widths.set(code, width);
|
|
this.firstChar = Math.min(code, this.firstChar);
|
|
this.lastChar = Math.max(code, this.lastChar);
|
|
}
|
|
}
|
|
|
|
return this.resources;
|
|
}
|
|
|
|
createAppearance(text, rect, rotation, fontSize, bgColor) {
|
|
const ctx = this._createContext();
|
|
const lines = [];
|
|
let maxWidth = -Infinity;
|
|
for (const line of text.split(/\r\n?|\n/)) {
|
|
lines.push(line);
|
|
// The line width isn't the sum of the char widths, because in some
|
|
// languages, like arabic, it'd be wrong because of ligatures.
|
|
const lineWidth = ctx.measureText(line).width;
|
|
maxWidth = Math.max(maxWidth, lineWidth);
|
|
for (const char of line.split("")) {
|
|
const code = char.charCodeAt(0);
|
|
let width = this.widths.get(code);
|
|
if (width === undefined) {
|
|
const metrics = ctx.measureText(char);
|
|
width = Math.ceil(metrics.width);
|
|
this.widths.set(code, width);
|
|
this.firstChar = Math.min(code, this.firstChar);
|
|
this.lastChar = Math.max(code, this.lastChar);
|
|
}
|
|
}
|
|
}
|
|
maxWidth *= fontSize / 1000;
|
|
|
|
const [x1, y1, x2, y2] = rect;
|
|
let w = x2 - x1;
|
|
let h = y2 - y1;
|
|
|
|
if (rotation % 180 !== 0) {
|
|
[w, h] = [h, w];
|
|
}
|
|
|
|
let hscale = 1;
|
|
if (maxWidth > w) {
|
|
hscale = w / maxWidth;
|
|
}
|
|
let vscale = 1;
|
|
const lineHeight = LINE_FACTOR * fontSize;
|
|
const lineDescent = LINE_DESCENT_FACTOR * fontSize;
|
|
const maxHeight = lineHeight * lines.length;
|
|
if (maxHeight > h) {
|
|
vscale = h / maxHeight;
|
|
}
|
|
const fscale = Math.min(hscale, vscale);
|
|
const newFontSize = fontSize * fscale;
|
|
|
|
const buffer = [
|
|
"q",
|
|
`0 0 ${numberToString(w)} ${numberToString(h)} re W n`,
|
|
`BT`,
|
|
`1 0 0 1 0 ${numberToString(h + lineDescent)} Tm 0 Tc ${getPdfColor(
|
|
bgColor,
|
|
/* isFill */ true
|
|
)}`,
|
|
`/${this.fontName.name} ${numberToString(newFontSize)} Tf`,
|
|
];
|
|
|
|
const vShift = numberToString(lineHeight);
|
|
for (const line of lines) {
|
|
buffer.push(`0 -${vShift} Td <${stringToUTF16HexString(line)}> Tj`);
|
|
}
|
|
buffer.push("ET", "Q");
|
|
const appearance = buffer.join("\n");
|
|
|
|
const appearanceStreamDict = new Dict(this.xref);
|
|
appearanceStreamDict.set("Subtype", Name.get("Form"));
|
|
appearanceStreamDict.set("Type", Name.get("XObject"));
|
|
appearanceStreamDict.set("BBox", [0, 0, w, h]);
|
|
appearanceStreamDict.set("Length", appearance.length);
|
|
appearanceStreamDict.set("Resources", this.resources);
|
|
|
|
if (rotation) {
|
|
const matrix = getRotationMatrix(rotation, w, h);
|
|
appearanceStreamDict.set("Matrix", matrix);
|
|
}
|
|
|
|
const ap = new StringStream(appearance);
|
|
ap.dict = appearanceStreamDict;
|
|
|
|
return ap;
|
|
}
|
|
}
|
|
|
|
export {
|
|
createDefaultAppearance,
|
|
FakeUnicodeFont,
|
|
getPdfColor,
|
|
parseDefaultAppearance,
|
|
};
|