Merge pull request #15587 from calixteman/save_unicode
[Annotation] Fix printing/saving for annotations containing some non-ascii chars and with no fonts to handle them (bug 1666824)
This commit is contained in:
commit
592d92424e
@ -22,7 +22,9 @@ import {
|
||||
AnnotationReplyType,
|
||||
AnnotationType,
|
||||
assert,
|
||||
BASELINE_FACTOR,
|
||||
escapeString,
|
||||
FeatureTest,
|
||||
getModificationDate,
|
||||
IDENTITY_MATRIX,
|
||||
isAscii,
|
||||
@ -33,7 +35,6 @@ import {
|
||||
shadow,
|
||||
stringToPDFString,
|
||||
stringToUTF16BEString,
|
||||
stringToUTF8String,
|
||||
unreachable,
|
||||
Util,
|
||||
warn,
|
||||
@ -41,10 +42,13 @@ import {
|
||||
import {
|
||||
collectActions,
|
||||
getInheritableProperty,
|
||||
getRotationMatrix,
|
||||
numberToString,
|
||||
stringToUTF16String,
|
||||
} from "./core_utils.js";
|
||||
import {
|
||||
createDefaultAppearance,
|
||||
FakeUnicodeFont,
|
||||
getPdfColor,
|
||||
parseDefaultAppearance,
|
||||
} from "./default_appearance.js";
|
||||
@ -143,6 +147,9 @@ class AnnotationFactory {
|
||||
needAppearances:
|
||||
!collectFields && acroFormDict.get("NeedAppearances") === true,
|
||||
pageIndex,
|
||||
isOffscreenCanvasSupported:
|
||||
FeatureTest.isOffscreenCanvasSupported &&
|
||||
pdfManager.evaluatorOptions.isOffscreenCanvasSupported,
|
||||
};
|
||||
|
||||
switch (subtype) {
|
||||
@ -268,7 +275,7 @@ class AnnotationFactory {
|
||||
baseFont.set("Subtype", Name.get("Type1"));
|
||||
baseFont.set("Encoding", Name.get("WinAnsiEncoding"));
|
||||
const buffer = [];
|
||||
baseFontRef = xref.getNewRef();
|
||||
baseFontRef = xref.getNewTemporaryRef();
|
||||
writeObject(baseFontRef, baseFont, buffer, null);
|
||||
dependencies.push({ ref: baseFontRef, data: buffer.join("") });
|
||||
}
|
||||
@ -301,6 +308,9 @@ class AnnotationFactory {
|
||||
|
||||
const xref = evaluator.xref;
|
||||
const promises = [];
|
||||
const isOffscreenCanvasSupported =
|
||||
FeatureTest.isOffscreenCanvasSupported &&
|
||||
evaluator.options.isOffscreenCanvasSupported;
|
||||
for (const annotation of annotations) {
|
||||
switch (annotation.annotationType) {
|
||||
case AnnotationEditorType.FREETEXT:
|
||||
@ -308,12 +318,15 @@ class AnnotationFactory {
|
||||
FreeTextAnnotation.createNewPrintAnnotation(xref, annotation, {
|
||||
evaluator,
|
||||
task,
|
||||
isOffscreenCanvasSupported,
|
||||
})
|
||||
);
|
||||
break;
|
||||
case AnnotationEditorType.INK:
|
||||
promises.push(
|
||||
InkAnnotation.createNewPrintAnnotation(xref, annotation)
|
||||
InkAnnotation.createNewPrintAnnotation(xref, annotation, {
|
||||
isOffscreenCanvasSupported,
|
||||
})
|
||||
);
|
||||
break;
|
||||
}
|
||||
@ -614,6 +627,17 @@ class Annotation {
|
||||
return { str, dir };
|
||||
}
|
||||
|
||||
setDefaultAppearance(params) {
|
||||
const defaultAppearance =
|
||||
getInheritableProperty({ dict: params.dict, key: "DA" }) ||
|
||||
params.acroForm.get("DA");
|
||||
this._defaultAppearance =
|
||||
typeof defaultAppearance === "string" ? defaultAppearance : "";
|
||||
this.data.defaultAppearanceData = parseDefaultAppearance(
|
||||
this._defaultAppearance
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the title.
|
||||
*
|
||||
@ -1449,20 +1473,25 @@ class MarkupAnnotation extends Annotation {
|
||||
}
|
||||
|
||||
static async createNewAnnotation(xref, annotation, dependencies, params) {
|
||||
const annotationRef = xref.getNewRef();
|
||||
const apRef = xref.getNewRef();
|
||||
const annotationDict = this.createNewDict(annotation, xref, { apRef });
|
||||
const annotationRef = xref.getNewTemporaryRef();
|
||||
const ap = await this.createNewAppearanceStream(annotation, xref, params);
|
||||
|
||||
const buffer = [];
|
||||
let transform = xref.encrypt
|
||||
? xref.encrypt.createCipherTransform(apRef.num, apRef.gen)
|
||||
: null;
|
||||
writeObject(apRef, ap, buffer, transform);
|
||||
dependencies.push({ ref: apRef, data: buffer.join("") });
|
||||
let annotationDict;
|
||||
|
||||
if (ap) {
|
||||
const apRef = xref.getNewTemporaryRef();
|
||||
annotationDict = this.createNewDict(annotation, xref, { apRef });
|
||||
const transform = xref.encrypt
|
||||
? xref.encrypt.createCipherTransform(apRef.num, apRef.gen)
|
||||
: null;
|
||||
writeObject(apRef, ap, buffer, transform);
|
||||
dependencies.push({ ref: apRef, data: buffer.join("") });
|
||||
} else {
|
||||
annotationDict = this.createNewDict(annotation, xref, {});
|
||||
}
|
||||
|
||||
buffer.length = 0;
|
||||
transform = xref.encrypt
|
||||
const transform = xref.encrypt
|
||||
? xref.encrypt.createCipherTransform(annotationRef.num, annotationRef.gen)
|
||||
: null;
|
||||
writeObject(annotationRef, annotationDict, buffer, transform);
|
||||
@ -1477,6 +1506,7 @@ class MarkupAnnotation extends Annotation {
|
||||
return new this.prototype.constructor({
|
||||
dict: annotationDict,
|
||||
xref,
|
||||
isOffscreenCanvasSupported: params.isOffscreenCanvasSupported,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1489,6 +1519,7 @@ class WidgetAnnotation extends Annotation {
|
||||
const data = this.data;
|
||||
this.ref = params.ref;
|
||||
this._needAppearances = params.needAppearances;
|
||||
this._isOffscreenCanvasSupported = params.isOffscreenCanvasSupported;
|
||||
|
||||
data.annotationType = AnnotationType.WIDGET;
|
||||
if (data.fieldName === undefined) {
|
||||
@ -1533,13 +1564,7 @@ class WidgetAnnotation extends Annotation {
|
||||
|
||||
data.alternativeText = stringToPDFString(dict.get("TU") || "");
|
||||
|
||||
const defaultAppearance =
|
||||
getInheritableProperty({ dict, key: "DA" }) || params.acroForm.get("DA");
|
||||
this._defaultAppearance =
|
||||
typeof defaultAppearance === "string" ? defaultAppearance : "";
|
||||
data.defaultAppearanceData = parseDefaultAppearance(
|
||||
this._defaultAppearance
|
||||
);
|
||||
this.setDefaultAppearance(params);
|
||||
|
||||
data.hasAppearance =
|
||||
(this._needAppearances &&
|
||||
@ -1612,19 +1637,6 @@ class WidgetAnnotation extends Annotation {
|
||||
return !!(this.data.fieldFlags & flag);
|
||||
}
|
||||
|
||||
static _getRotationMatrix(rotation, width, height) {
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
return [0, 1, -1, 0, width, 0];
|
||||
case 180:
|
||||
return [-1, 0, 0, -1, width, height];
|
||||
case 270:
|
||||
return [0, -1, 1, 0, 0, height];
|
||||
default:
|
||||
throw new Error("Invalid rotation");
|
||||
}
|
||||
}
|
||||
|
||||
getRotationMatrix(annotationStorage) {
|
||||
const storageEntry = annotationStorage
|
||||
? annotationStorage.get(this.data.id)
|
||||
@ -1641,7 +1653,7 @@ class WidgetAnnotation extends Annotation {
|
||||
const width = this.data.rect[2] - this.data.rect[0];
|
||||
const height = this.data.rect[3] - this.data.rect[1];
|
||||
|
||||
return WidgetAnnotation._getRotationMatrix(rotation, width, height);
|
||||
return getRotationMatrix(rotation, width, height);
|
||||
}
|
||||
|
||||
getBorderAndBackgroundAppearances(annotationStorage) {
|
||||
@ -1712,6 +1724,7 @@ class WidgetAnnotation extends Annotation {
|
||||
const content = await this._getAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
intent,
|
||||
annotationStorage
|
||||
);
|
||||
if (this.appearance && content === null) {
|
||||
@ -1824,89 +1837,121 @@ class WidgetAnnotation extends Annotation {
|
||||
rotation = this.rotation;
|
||||
}
|
||||
|
||||
let appearance = await this._getAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
annotationStorage
|
||||
);
|
||||
if (appearance === null) {
|
||||
return null;
|
||||
let appearance = null;
|
||||
if (!this._needAppearances) {
|
||||
appearance = await this._getAppearance(
|
||||
evaluator,
|
||||
task,
|
||||
RenderingIntentFlag.SAVE,
|
||||
annotationStorage
|
||||
);
|
||||
if (appearance === null) {
|
||||
// Appearance didn't change.
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
// No need to create an appearance: the pdf has the flag /NeedAppearances
|
||||
// which means that it's up to the reader to produce an appearance.
|
||||
}
|
||||
|
||||
let needAppearances = false;
|
||||
if (appearance && appearance.needAppearances) {
|
||||
needAppearances = true;
|
||||
appearance = null;
|
||||
}
|
||||
|
||||
const { xref } = evaluator;
|
||||
|
||||
const dict = xref.fetchIfRef(this.ref);
|
||||
if (!(dict instanceof Dict)) {
|
||||
const originalDict = xref.fetchIfRef(this.ref);
|
||||
if (!(originalDict instanceof Dict)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const bbox = [
|
||||
0,
|
||||
0,
|
||||
this.data.rect[2] - this.data.rect[0],
|
||||
this.data.rect[3] - this.data.rect[1],
|
||||
];
|
||||
const dict = new Dict(xref);
|
||||
for (const key of originalDict.getKeys()) {
|
||||
if (key !== "AP") {
|
||||
dict.set(key, originalDict.getRaw(key));
|
||||
}
|
||||
}
|
||||
|
||||
const xfa = {
|
||||
path: stringToPDFString(dict.get("T") || ""),
|
||||
value,
|
||||
};
|
||||
|
||||
const newRef = xref.getNewRef();
|
||||
const AP = new Dict(xref);
|
||||
AP.set("N", newRef);
|
||||
|
||||
const encrypt = xref.encrypt;
|
||||
let originalTransform = null;
|
||||
let newTransform = null;
|
||||
if (encrypt) {
|
||||
originalTransform = encrypt.createCipherTransform(
|
||||
this.ref.num,
|
||||
this.ref.gen
|
||||
);
|
||||
newTransform = encrypt.createCipherTransform(newRef.num, newRef.gen);
|
||||
appearance = newTransform.encryptString(appearance);
|
||||
}
|
||||
|
||||
const encoder = val => (isAscii(val) ? val : stringToUTF16BEString(val));
|
||||
dict.set("V", Array.isArray(value) ? value.map(encoder) : encoder(value));
|
||||
dict.set("AP", AP);
|
||||
dict.set("M", `D:${getModificationDate()}`);
|
||||
|
||||
const maybeMK = this._getMKDict(rotation);
|
||||
if (maybeMK) {
|
||||
dict.set("MK", maybeMK);
|
||||
}
|
||||
|
||||
const appearanceDict = new Dict(xref);
|
||||
appearanceDict.set("Length", appearance.length);
|
||||
appearanceDict.set("Subtype", Name.get("Form"));
|
||||
appearanceDict.set("Resources", this._getSaveFieldResources(xref));
|
||||
appearanceDict.set("BBox", bbox);
|
||||
const encrypt = xref.encrypt;
|
||||
const originalTransform = encrypt
|
||||
? encrypt.createCipherTransform(this.ref.num, this.ref.gen)
|
||||
: null;
|
||||
|
||||
const rotationMatrix = this.getRotationMatrix(annotationStorage);
|
||||
if (rotationMatrix !== IDENTITY_MATRIX) {
|
||||
// The matrix isn't the identity one.
|
||||
appearanceDict.set("Matrix", rotationMatrix);
|
||||
}
|
||||
|
||||
const bufferOriginal = [`${this.ref.num} ${this.ref.gen} obj\n`];
|
||||
writeDict(dict, bufferOriginal, originalTransform);
|
||||
bufferOriginal.push("\nendobj\n");
|
||||
|
||||
const bufferNew = [`${newRef.num} ${newRef.gen} obj\n`];
|
||||
writeDict(appearanceDict, bufferNew, newTransform);
|
||||
bufferNew.push(" stream\n", appearance, "\nendstream\nendobj\n");
|
||||
|
||||
return [
|
||||
const buffer = [];
|
||||
const changes = [
|
||||
// data for the original object
|
||||
// V field changed + reference for new AP
|
||||
{ ref: this.ref, data: bufferOriginal.join(""), xfa },
|
||||
// data for the new AP
|
||||
{ ref: newRef, data: bufferNew.join(""), xfa: null },
|
||||
{ ref: this.ref, data: "", xfa, needAppearances },
|
||||
];
|
||||
if (appearance !== null) {
|
||||
const newRef = xref.getNewTemporaryRef();
|
||||
const AP = new Dict(xref);
|
||||
dict.set("AP", AP);
|
||||
AP.set("N", newRef);
|
||||
|
||||
let newTransform = null;
|
||||
if (encrypt) {
|
||||
newTransform = encrypt.createCipherTransform(newRef.num, newRef.gen);
|
||||
appearance = newTransform.encryptString(appearance);
|
||||
}
|
||||
|
||||
const resources = this._getSaveFieldResources(xref);
|
||||
const appearanceStream = new StringStream(appearance);
|
||||
const appearanceDict = (appearanceStream.dict = new Dict(xref));
|
||||
appearanceDict.set("Length", appearance.length);
|
||||
appearanceDict.set("Subtype", Name.get("Form"));
|
||||
appearanceDict.set("Resources", resources);
|
||||
appearanceDict.set("BBox", [
|
||||
0,
|
||||
0,
|
||||
this.data.rect[2] - this.data.rect[0],
|
||||
this.data.rect[3] - this.data.rect[1],
|
||||
]);
|
||||
|
||||
const rotationMatrix = this.getRotationMatrix(annotationStorage);
|
||||
if (rotationMatrix !== IDENTITY_MATRIX) {
|
||||
// The matrix isn't the identity one.
|
||||
appearanceDict.set("Matrix", rotationMatrix);
|
||||
}
|
||||
|
||||
writeObject(newRef, appearanceStream, buffer, newTransform);
|
||||
|
||||
changes.push(
|
||||
// data for the new AP
|
||||
{
|
||||
ref: newRef,
|
||||
data: buffer.join(""),
|
||||
xfa: null,
|
||||
needAppearances: false,
|
||||
}
|
||||
);
|
||||
buffer.length = 0;
|
||||
}
|
||||
|
||||
dict.set("M", `D:${getModificationDate()}`);
|
||||
writeObject(this.ref, dict, buffer, originalTransform);
|
||||
|
||||
changes[0].data = buffer.join("");
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
async _getAppearance(evaluator, task, annotationStorage) {
|
||||
async _getAppearance(evaluator, task, intent, annotationStorage) {
|
||||
const isPassword = this.hasFieldFlag(AnnotationFieldFlag.PASSWORD);
|
||||
if (isPassword) {
|
||||
return null;
|
||||
@ -1961,12 +2006,30 @@ class WidgetAnnotation extends Annotation {
|
||||
}
|
||||
|
||||
let lineCount = -1;
|
||||
let lines;
|
||||
|
||||
// We could have a text containing for example some sequences of chars and
|
||||
// their diacritics (e.g. "é".normalize("NFKD") shows 1 char when it's 2).
|
||||
// Positioning diacritics is really something we don't want to do here.
|
||||
// So if a font has a glyph for a acute accent and one for "e" then we won't
|
||||
// get any encoding issues but we'll render "e" and then "´".
|
||||
// It's why we normalize the string. We use NFC to preserve the initial
|
||||
// string, (e.g. "²".normalize("NFC") === "²"
|
||||
// but "²".normalize("NFKC") === "2").
|
||||
//
|
||||
// TODO: it isn't a perfect solution, some chars like "ẹ́" will be
|
||||
// decomposed into two chars ("ẹ" and "´"), so we should detect such
|
||||
// situations and then use either FakeUnicodeFont or set the
|
||||
// /NeedAppearances flag.
|
||||
if (this.data.multiLine) {
|
||||
lineCount = value.split(/\r\n|\r|\n/).length;
|
||||
lines = value.split(/\r\n?|\n/).map(line => line.normalize("NFC"));
|
||||
lineCount = lines.length;
|
||||
} else {
|
||||
lines = [value.replace(/\r\n?|\n/, "").normalize("NFC")];
|
||||
}
|
||||
|
||||
const defaultPadding = 2;
|
||||
const hPadding = defaultPadding;
|
||||
const defaultPadding = 1;
|
||||
const defaultHPadding = 2;
|
||||
let totalHeight = this.data.rect[3] - this.data.rect[1];
|
||||
let totalWidth = this.data.rect[2] - this.data.rect[0];
|
||||
|
||||
@ -1985,23 +2048,107 @@ class WidgetAnnotation extends Annotation {
|
||||
);
|
||||
}
|
||||
|
||||
const font = await WidgetAnnotation._getFontData(
|
||||
let font = await WidgetAnnotation._getFontData(
|
||||
evaluator,
|
||||
task,
|
||||
this.data.defaultAppearanceData,
|
||||
this._fieldResources.mergedResources
|
||||
);
|
||||
const [defaultAppearance, fontSize] = this._computeFontSize(
|
||||
totalHeight - defaultPadding,
|
||||
totalWidth - 2 * hPadding,
|
||||
value,
|
||||
font,
|
||||
lineCount
|
||||
);
|
||||
|
||||
let defaultAppearance, fontSize, lineHeight;
|
||||
const encodedLines = [];
|
||||
let encodingError = false;
|
||||
for (const line of lines) {
|
||||
const encodedString = font.encodeString(line);
|
||||
if (encodedString.length > 1) {
|
||||
encodingError = true;
|
||||
}
|
||||
encodedLines.push(encodedString.join(""));
|
||||
}
|
||||
|
||||
if (encodingError && intent & RenderingIntentFlag.SAVE) {
|
||||
// We don't have a way to render the field, so we just rely on the
|
||||
// /NeedAppearances trick to let the different sofware correctly render
|
||||
// this pdf.
|
||||
return { needAppearances: true };
|
||||
}
|
||||
|
||||
// We check that the font is able to encode the string.
|
||||
if (encodingError && this._isOffscreenCanvasSupported) {
|
||||
// If it can't then we fallback on fake unicode font (mapped to sans-serif
|
||||
// for the rendering).
|
||||
// It means that a printed form can be rendered differently (it depends on
|
||||
// the sans-serif font) but at least we've something to render.
|
||||
// In an ideal world the associated font should correctly handle the
|
||||
// possible chars but a user can add a smiley or whatever.
|
||||
// We could try to embed a font but it means that we must have access
|
||||
// to the raw font file.
|
||||
const fontFamily = this.data.comb ? "monospace" : "sans-serif";
|
||||
const fakeUnicodeFont = new FakeUnicodeFont(evaluator.xref, fontFamily);
|
||||
const resources = fakeUnicodeFont.createFontResources(lines.join(""));
|
||||
const newFont = resources.getRaw("Font");
|
||||
|
||||
if (this._fieldResources.mergedResources.has("Font")) {
|
||||
const oldFont = this._fieldResources.mergedResources.get("Font");
|
||||
for (const key of newFont.getKeys()) {
|
||||
oldFont.set(key, newFont.getRaw(key));
|
||||
}
|
||||
} else {
|
||||
this._fieldResources.mergedResources.set("Font", newFont);
|
||||
}
|
||||
|
||||
const fontName = fakeUnicodeFont.fontName.name;
|
||||
font = await WidgetAnnotation._getFontData(
|
||||
evaluator,
|
||||
task,
|
||||
{ fontName, fontSize: 0 },
|
||||
resources
|
||||
);
|
||||
|
||||
for (let i = 0, ii = encodedLines.length; i < ii; i++) {
|
||||
encodedLines[i] = stringToUTF16String(lines[i]);
|
||||
}
|
||||
|
||||
const savedDefaultAppearance = Object.assign(
|
||||
Object.create(null),
|
||||
this.data.defaultAppearanceData
|
||||
);
|
||||
this.data.defaultAppearanceData.fontSize = 0;
|
||||
this.data.defaultAppearanceData.fontName = fontName;
|
||||
|
||||
[defaultAppearance, fontSize, lineHeight] = this._computeFontSize(
|
||||
totalHeight - 2 * defaultPadding,
|
||||
totalWidth - 2 * defaultHPadding,
|
||||
value,
|
||||
font,
|
||||
lineCount
|
||||
);
|
||||
|
||||
this.data.defaultAppearanceData = savedDefaultAppearance;
|
||||
} else {
|
||||
if (!this._isOffscreenCanvasSupported) {
|
||||
warn(
|
||||
"_getAppearance: OffscreenCanvas is not supported, annotation may not render correctly."
|
||||
);
|
||||
}
|
||||
|
||||
[defaultAppearance, fontSize, lineHeight] = this._computeFontSize(
|
||||
totalHeight - 2 * defaultPadding,
|
||||
totalWidth - 2 * defaultHPadding,
|
||||
value,
|
||||
font,
|
||||
lineCount
|
||||
);
|
||||
}
|
||||
|
||||
let descent = font.descent;
|
||||
if (isNaN(descent)) {
|
||||
descent = 0;
|
||||
descent = BASELINE_FACTOR * lineHeight;
|
||||
} else {
|
||||
descent = Math.max(
|
||||
BASELINE_FACTOR * lineHeight,
|
||||
Math.abs(descent) * fontSize
|
||||
);
|
||||
}
|
||||
|
||||
// Take into account the space we have to compute the default vertical
|
||||
@ -2010,59 +2157,64 @@ class WidgetAnnotation extends Annotation {
|
||||
Math.floor((totalHeight - fontSize) / 2),
|
||||
defaultPadding
|
||||
);
|
||||
const vPadding = defaultVPadding + Math.abs(descent) * fontSize;
|
||||
const alignment = this.data.textAlignment;
|
||||
|
||||
if (this.data.multiLine) {
|
||||
return this._getMultilineAppearance(
|
||||
defaultAppearance,
|
||||
value,
|
||||
encodedLines,
|
||||
font,
|
||||
fontSize,
|
||||
totalWidth,
|
||||
totalHeight,
|
||||
alignment,
|
||||
hPadding,
|
||||
vPadding,
|
||||
defaultHPadding,
|
||||
defaultVPadding,
|
||||
descent,
|
||||
lineHeight,
|
||||
annotationStorage
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: need to handle chars which are not in the font.
|
||||
const encodedString = font.encodeString(value).join("");
|
||||
|
||||
if (this.data.comb) {
|
||||
return this._getCombAppearance(
|
||||
defaultAppearance,
|
||||
font,
|
||||
encodedString,
|
||||
encodedLines[0],
|
||||
fontSize,
|
||||
totalWidth,
|
||||
hPadding,
|
||||
vPadding,
|
||||
totalHeight,
|
||||
defaultHPadding,
|
||||
defaultVPadding,
|
||||
descent,
|
||||
lineHeight,
|
||||
annotationStorage
|
||||
);
|
||||
}
|
||||
|
||||
const bottomPadding = defaultVPadding + descent;
|
||||
if (alignment === 0 || alignment > 2) {
|
||||
// Left alignment: nothing to do
|
||||
return (
|
||||
`/Tx BMC q ${colors}BT ` +
|
||||
defaultAppearance +
|
||||
` 1 0 0 1 ${hPadding} ${vPadding} Tm (${escapeString(
|
||||
encodedString
|
||||
)}) Tj` +
|
||||
` 1 0 0 1 ${numberToString(defaultHPadding)} ${numberToString(
|
||||
bottomPadding
|
||||
)} Tm (${escapeString(encodedLines[0])}) Tj` +
|
||||
" ET Q EMC"
|
||||
);
|
||||
}
|
||||
|
||||
const prevInfo = { shift: 0 };
|
||||
const renderedText = this._renderText(
|
||||
encodedString,
|
||||
encodedLines[0],
|
||||
font,
|
||||
fontSize,
|
||||
totalWidth,
|
||||
alignment,
|
||||
hPadding,
|
||||
vPadding
|
||||
prevInfo,
|
||||
defaultHPadding,
|
||||
bottomPadding
|
||||
);
|
||||
return (
|
||||
`/Tx BMC q ${colors}BT ` +
|
||||
@ -2105,6 +2257,9 @@ class WidgetAnnotation extends Annotation {
|
||||
|
||||
_computeFontSize(height, width, text, font, lineCount) {
|
||||
let { fontSize } = this.data.defaultAppearanceData;
|
||||
let lineHeight = (fontSize || 12) * LINE_FACTOR,
|
||||
numberOfLines = Math.round(height / lineHeight);
|
||||
|
||||
if (!fontSize) {
|
||||
// A zero value for size means that the font shall be auto-sized:
|
||||
// its size shall be computed as a function of the height of the
|
||||
@ -2115,8 +2270,12 @@ class WidgetAnnotation extends Annotation {
|
||||
if (lineCount === -1) {
|
||||
const textWidth = this._getTextWidth(text, font);
|
||||
fontSize = roundWithTwoDigits(
|
||||
Math.min(height / LINE_FACTOR, width / textWidth)
|
||||
Math.min(
|
||||
height / LINE_FACTOR,
|
||||
textWidth > width ? width / textWidth : Infinity
|
||||
)
|
||||
);
|
||||
numberOfLines = 1;
|
||||
} else {
|
||||
const lines = text.split(/\r\n?|\n/);
|
||||
const cachedLines = [];
|
||||
@ -2152,9 +2311,6 @@ class WidgetAnnotation extends Annotation {
|
||||
// a font size equal to 12 (this is the default font size in
|
||||
// Acrobat).
|
||||
// Then we'll adjust font size to what we have really.
|
||||
fontSize = 12;
|
||||
let lineHeight = fontSize * LINE_FACTOR;
|
||||
let numberOfLines = Math.round(height / lineHeight);
|
||||
numberOfLines = Math.max(numberOfLines, lineCount);
|
||||
|
||||
while (true) {
|
||||
@ -2177,10 +2333,24 @@ class WidgetAnnotation extends Annotation {
|
||||
fontColor,
|
||||
});
|
||||
}
|
||||
return [this._defaultAppearance, fontSize];
|
||||
|
||||
return [this._defaultAppearance, fontSize, height / numberOfLines];
|
||||
}
|
||||
|
||||
_renderText(text, font, fontSize, totalWidth, alignment, hPadding, vPadding) {
|
||||
_renderText(
|
||||
text,
|
||||
font,
|
||||
fontSize,
|
||||
totalWidth,
|
||||
alignment,
|
||||
prevInfo,
|
||||
hPadding,
|
||||
vPadding
|
||||
) {
|
||||
// TODO: we need to take into account (if possible) how the text
|
||||
// is rendered. For example in arabic, the cumulated width of some
|
||||
// glyphs isn't equal to the width of the rendered glyphs because
|
||||
// of ligatures.
|
||||
let shift;
|
||||
if (alignment === 1) {
|
||||
// Center
|
||||
@ -2193,10 +2363,11 @@ class WidgetAnnotation extends Annotation {
|
||||
} else {
|
||||
shift = hPadding;
|
||||
}
|
||||
shift = numberToString(shift);
|
||||
const shiftStr = numberToString(shift - prevInfo.shift);
|
||||
prevInfo.shift = shift;
|
||||
vPadding = numberToString(vPadding);
|
||||
|
||||
return `${shift} ${vPadding} Td (${escapeString(text)}) Tj`;
|
||||
return `${shiftStr} ${vPadding} Td (${escapeString(text)}) Tj`;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -2296,32 +2467,39 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
||||
defaultAppearance,
|
||||
font,
|
||||
text,
|
||||
fontSize,
|
||||
width,
|
||||
height,
|
||||
hPadding,
|
||||
vPadding,
|
||||
descent,
|
||||
lineHeight,
|
||||
annotationStorage
|
||||
) {
|
||||
const combWidth = numberToString(width / this.data.maxLen);
|
||||
const combWidth = width / this.data.maxLen;
|
||||
// Empty or it has a trailing whitespace.
|
||||
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
|
||||
|
||||
const buf = [];
|
||||
const positions = font.getCharPositions(text);
|
||||
for (const [start, end] of positions) {
|
||||
buf.push(`(${escapeString(text.substring(start, end))}) Tj`);
|
||||
}
|
||||
|
||||
// Empty or it has a trailing whitespace.
|
||||
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
|
||||
const renderedComb = buf.join(` ${combWidth} 0 Td `);
|
||||
const renderedComb = buf.join(` ${numberToString(combWidth)} 0 Td `);
|
||||
return (
|
||||
`/Tx BMC q ${colors}BT ` +
|
||||
defaultAppearance +
|
||||
` 1 0 0 1 ${hPadding} ${vPadding} Tm ${renderedComb}` +
|
||||
` 1 0 0 1 ${numberToString(hPadding)} ${numberToString(
|
||||
vPadding + descent
|
||||
)} Tm ${renderedComb}` +
|
||||
" ET Q EMC"
|
||||
);
|
||||
}
|
||||
|
||||
_getMultilineAppearance(
|
||||
defaultAppearance,
|
||||
text,
|
||||
lines,
|
||||
font,
|
||||
fontSize,
|
||||
width,
|
||||
@ -2329,15 +2507,20 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
||||
alignment,
|
||||
hPadding,
|
||||
vPadding,
|
||||
descent,
|
||||
lineHeight,
|
||||
annotationStorage
|
||||
) {
|
||||
const lines = text.split(/\r\n?|\n/);
|
||||
const buf = [];
|
||||
const totalWidth = width - 2 * hPadding;
|
||||
for (const line of lines) {
|
||||
const prevInfo = { shift: 0 };
|
||||
for (let i = 0, ii = lines.length; i < ii; i++) {
|
||||
const line = lines[i];
|
||||
const chunks = this._splitLine(line, font, fontSize, totalWidth);
|
||||
for (const chunk of chunks) {
|
||||
const padding = buf.length === 0 ? hPadding : 0;
|
||||
for (let j = 0, jj = chunks.length; j < jj; j++) {
|
||||
const chunk = chunks[j];
|
||||
const vShift =
|
||||
i === 0 && j === 0 ? -vPadding - (lineHeight - descent) : -lineHeight;
|
||||
buf.push(
|
||||
this._renderText(
|
||||
chunk,
|
||||
@ -2345,29 +2528,28 @@ class TextWidgetAnnotation extends WidgetAnnotation {
|
||||
fontSize,
|
||||
width,
|
||||
alignment,
|
||||
padding,
|
||||
-fontSize // <0 because a line is below the previous one
|
||||
prevInfo,
|
||||
hPadding,
|
||||
vShift
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const renderedText = buf.join("\n");
|
||||
|
||||
// Empty or it has a trailing whitespace.
|
||||
const colors = this.getBorderAndBackgroundAppearances(annotationStorage);
|
||||
const renderedText = buf.join("\n");
|
||||
|
||||
return (
|
||||
`/Tx BMC q ${colors}BT ` +
|
||||
defaultAppearance +
|
||||
` 1 0 0 1 0 ${height} Tm ${renderedText}` +
|
||||
` 1 0 0 1 0 ${numberToString(height)} Tm ${renderedText}` +
|
||||
" ET Q EMC"
|
||||
);
|
||||
}
|
||||
|
||||
_splitLine(line, font, fontSize, width, cache = {}) {
|
||||
// TODO: need to handle chars which are not in the font.
|
||||
line = cache.line || font.encodeString(line).join("");
|
||||
line = cache.line || line;
|
||||
|
||||
const glyphs = cache.glyphs || font.charsToGlyphs(line);
|
||||
|
||||
@ -3031,9 +3213,9 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
};
|
||||
}
|
||||
|
||||
async _getAppearance(evaluator, task, annotationStorage) {
|
||||
async _getAppearance(evaluator, task, intent, annotationStorage) {
|
||||
if (this.data.combo) {
|
||||
return super._getAppearance(evaluator, task, annotationStorage);
|
||||
return super._getAppearance(evaluator, task, intent, annotationStorage);
|
||||
}
|
||||
|
||||
let exportedValue, rotation;
|
||||
@ -3061,8 +3243,8 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
exportedValue = [exportedValue];
|
||||
}
|
||||
|
||||
const defaultPadding = 2;
|
||||
const hPadding = defaultPadding;
|
||||
const defaultPadding = 1;
|
||||
const defaultHPadding = 2;
|
||||
let totalHeight = this.data.rect[3] - this.data.rect[1];
|
||||
let totalWidth = this.data.rect[2] - this.data.rect[0];
|
||||
|
||||
@ -3113,7 +3295,7 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
|
||||
[defaultAppearance, fontSize] = this._computeFontSize(
|
||||
lineHeight,
|
||||
totalWidth - 2 * hPadding,
|
||||
totalWidth - 2 * defaultHPadding,
|
||||
value,
|
||||
font,
|
||||
-1
|
||||
@ -3159,9 +3341,9 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
}
|
||||
buf.push("BT", defaultAppearance, `1 0 0 1 0 ${totalHeight} Tm`);
|
||||
|
||||
const prevInfo = { shift: 0 };
|
||||
for (let i = firstIndex; i < end; i++) {
|
||||
const { displayValue } = this.data.options[i];
|
||||
const hpadding = i === firstIndex ? hPadding : 0;
|
||||
const vpadding = i === firstIndex ? vPadding : 0;
|
||||
buf.push(
|
||||
this._renderText(
|
||||
@ -3170,7 +3352,8 @@ class ChoiceWidgetAnnotation extends WidgetAnnotation {
|
||||
fontSize,
|
||||
totalWidth,
|
||||
0,
|
||||
hpadding,
|
||||
prevInfo,
|
||||
defaultHPadding,
|
||||
-lineHeight + vpadding
|
||||
)
|
||||
);
|
||||
@ -3326,6 +3509,26 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
||||
super(parameters);
|
||||
|
||||
this.data.annotationType = AnnotationType.FREETEXT;
|
||||
this.setDefaultAppearance(parameters);
|
||||
if (!this.appearance && this._isOffscreenCanvasSupported) {
|
||||
const fakeUnicodeFont = new FakeUnicodeFont(
|
||||
parameters.xref,
|
||||
"sans-serif"
|
||||
);
|
||||
const fontData = this.data.defaultAppearanceData;
|
||||
this.appearance = fakeUnicodeFont.createAppearance(
|
||||
this._contents.str,
|
||||
this.rectangle,
|
||||
this.rotation,
|
||||
fontData.fontSize || 10,
|
||||
fontData.fontColor
|
||||
);
|
||||
this._streams.push(this.appearance, FakeUnicodeFont.toUnicodeStream);
|
||||
} else if (!this._isOffscreenCanvasSupported) {
|
||||
warn(
|
||||
"FreeTextAnnotation: OffscreenCanvas is not supported, annotation may not render correctly."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
get hasTextContent() {
|
||||
@ -3341,22 +3544,27 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
||||
freetext.set("Rect", rect);
|
||||
const da = `/Helv ${fontSize} Tf ${getPdfColor(color, /* isFill */ true)}`;
|
||||
freetext.set("DA", da);
|
||||
freetext.set("Contents", value);
|
||||
freetext.set(
|
||||
"Contents",
|
||||
isAscii(value) ? value : stringToUTF16BEString(value)
|
||||
);
|
||||
freetext.set("F", 4);
|
||||
freetext.set("Border", [0, 0, 0]);
|
||||
freetext.set("Rotate", rotation);
|
||||
|
||||
if (user) {
|
||||
freetext.set("T", stringToUTF8String(user));
|
||||
freetext.set("T", isAscii(user) ? user : stringToUTF16BEString(user));
|
||||
}
|
||||
|
||||
const n = new Dict(xref);
|
||||
freetext.set("AP", n);
|
||||
if (apRef || ap) {
|
||||
const n = new Dict(xref);
|
||||
freetext.set("AP", n);
|
||||
|
||||
if (apRef) {
|
||||
n.set("N", apRef);
|
||||
} else {
|
||||
n.set("N", ap);
|
||||
if (apRef) {
|
||||
n.set("N", apRef);
|
||||
} else {
|
||||
n.set("N", ap);
|
||||
}
|
||||
}
|
||||
|
||||
return freetext;
|
||||
@ -3404,7 +3612,12 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
||||
let totalWidth = -Infinity;
|
||||
const encodedLines = [];
|
||||
for (let line of lines) {
|
||||
line = helv.encodeString(line).join("");
|
||||
const encoded = helv.encodeString(line);
|
||||
if (encoded.length > 1) {
|
||||
// The font doesn't contain all the chars.
|
||||
return null;
|
||||
}
|
||||
line = encoded.join("");
|
||||
encodedLines.push(line);
|
||||
let lineWidth = 0;
|
||||
const glyphs = helv.charsToGlyphs(line);
|
||||
@ -3454,7 +3667,7 @@ class FreeTextAnnotation extends MarkupAnnotation {
|
||||
appearanceStreamDict.set("Resources", resources);
|
||||
|
||||
if (rotation) {
|
||||
const matrix = WidgetAnnotation._getRotationMatrix(rotation, w, h);
|
||||
const matrix = getRotationMatrix(rotation, w, h);
|
||||
appearanceStreamDict.set("Matrix", matrix);
|
||||
}
|
||||
|
||||
@ -3897,7 +4110,7 @@ class InkAnnotation extends MarkupAnnotation {
|
||||
appearanceStreamDict.set("Length", appearance.length);
|
||||
|
||||
if (rotation) {
|
||||
const matrix = WidgetAnnotation._getRotationMatrix(rotation, w, h);
|
||||
const matrix = getRotationMatrix(rotation, w, h);
|
||||
appearanceStreamDict.set("Matrix", matrix);
|
||||
}
|
||||
|
||||
|
@ -572,6 +572,43 @@ function getNewAnnotationsMap(annotationStorage) {
|
||||
return newAnnotationsByPage.size > 0 ? newAnnotationsByPage : null;
|
||||
}
|
||||
|
||||
function stringToUTF16HexString(str) {
|
||||
const buf = [];
|
||||
for (let i = 0, ii = str.length; i < ii; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
buf.push(
|
||||
((char >> 8) & 0xff).toString(16).padStart(2, "0"),
|
||||
(char & 0xff).toString(16).padStart(2, "0")
|
||||
);
|
||||
}
|
||||
return buf.join("");
|
||||
}
|
||||
|
||||
function stringToUTF16String(str) {
|
||||
const buf = [];
|
||||
for (let i = 0, ii = str.length; i < ii; i++) {
|
||||
const char = str.charCodeAt(i);
|
||||
buf.push(
|
||||
String.fromCharCode((char >> 8) & 0xff),
|
||||
String.fromCharCode(char & 0xff)
|
||||
);
|
||||
}
|
||||
return buf.join("");
|
||||
}
|
||||
|
||||
function getRotationMatrix(rotation, width, height) {
|
||||
switch (rotation) {
|
||||
case 90:
|
||||
return [0, 1, -1, 0, width, 0];
|
||||
case 180:
|
||||
return [-1, 0, 0, -1, width, height];
|
||||
case 270:
|
||||
return [0, -1, 1, 0, 0, height];
|
||||
default:
|
||||
throw new Error("Invalid rotation");
|
||||
}
|
||||
}
|
||||
|
||||
export {
|
||||
collectActions,
|
||||
DocStats,
|
||||
@ -581,6 +618,7 @@ export {
|
||||
getInheritableProperty,
|
||||
getLookupTableFactory,
|
||||
getNewAnnotationsMap,
|
||||
getRotationMatrix,
|
||||
isWhiteSpace,
|
||||
log2,
|
||||
MissingDataException,
|
||||
@ -592,6 +630,8 @@ export {
|
||||
readUint16,
|
||||
readUint32,
|
||||
recoverJsURL,
|
||||
stringToUTF16HexString,
|
||||
stringToUTF16String,
|
||||
toRomanNumerals,
|
||||
validateCSSFont,
|
||||
XRefEntryException,
|
||||
|
@ -13,11 +13,16 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { escapePDFName, numberToString } from "./core_utils.js";
|
||||
import { OPS, warn } from "../shared/util.js";
|
||||
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 { Name } from "./primitives.js";
|
||||
import { StringStream } from "./stream.js";
|
||||
|
||||
class DefaultAppearanceEvaluator extends EvaluatorPreprocessor {
|
||||
@ -101,4 +106,250 @@ function createDefaultAppearance({ fontSize, fontName, fontColor }) {
|
||||
)}`;
|
||||
}
|
||||
|
||||
export { createDefaultAppearance, getPdfColor, parseDefaultAppearance };
|
||||
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,
|
||||
};
|
||||
|
@ -93,6 +93,7 @@ const EXPORT_DATA_PROPERTIES = [
|
||||
"fallbackName",
|
||||
"fontMatrix",
|
||||
"fontType",
|
||||
"isInvalidPDFjsFont",
|
||||
"isType3Font",
|
||||
"italic",
|
||||
"loadedName",
|
||||
@ -952,13 +953,17 @@ class Font {
|
||||
this.type = type;
|
||||
this.subtype = subtype;
|
||||
|
||||
let fallbackName = "sans-serif";
|
||||
if (this.isMonospace) {
|
||||
fallbackName = "monospace";
|
||||
const matches = name.match(/^InvalidPDFjsFont_(.*)_\d+$/);
|
||||
this.isInvalidPDFjsFont = !!matches;
|
||||
if (this.isInvalidPDFjsFont) {
|
||||
this.fallbackName = matches[1];
|
||||
} else if (this.isMonospace) {
|
||||
this.fallbackName = "monospace";
|
||||
} else if (this.isSerifFont) {
|
||||
fallbackName = "serif";
|
||||
this.fallbackName = "serif";
|
||||
} else {
|
||||
this.fallbackName = "sans-serif";
|
||||
}
|
||||
this.fallbackName = fallbackName;
|
||||
|
||||
this.differences = properties.differences;
|
||||
this.widths = properties.widths;
|
||||
|
@ -625,6 +625,11 @@ class WorkerMessageHandler {
|
||||
}
|
||||
}
|
||||
|
||||
const needAppearances =
|
||||
acroFormRef &&
|
||||
acroForm instanceof Dict &&
|
||||
newRefs.some(ref => ref.needAppearances);
|
||||
|
||||
const xfa = (acroForm instanceof Dict && acroForm.get("XFA")) || null;
|
||||
let xfaDatasetsRef = null;
|
||||
let hasXfaDatasetsEntry = false;
|
||||
@ -632,15 +637,13 @@ class WorkerMessageHandler {
|
||||
for (let i = 0, ii = xfa.length; i < ii; i += 2) {
|
||||
if (xfa[i] === "datasets") {
|
||||
xfaDatasetsRef = xfa[i + 1];
|
||||
acroFormRef = null;
|
||||
hasXfaDatasetsEntry = true;
|
||||
}
|
||||
}
|
||||
if (xfaDatasetsRef === null) {
|
||||
xfaDatasetsRef = xref.getNewRef();
|
||||
xfaDatasetsRef = xref.getNewTemporaryRef();
|
||||
}
|
||||
} else if (xfa) {
|
||||
acroFormRef = null;
|
||||
// TODO: Support XFA streams.
|
||||
warn("Unsupported XFA type.");
|
||||
}
|
||||
@ -661,7 +664,7 @@ class WorkerMessageHandler {
|
||||
newXrefInfo = {
|
||||
rootRef: xref.trailer.getRaw("Root") || null,
|
||||
encryptRef: xref.trailer.getRaw("Encrypt") || null,
|
||||
newRef: xref.getNewRef(),
|
||||
newRef: xref.getNewTemporaryRef(),
|
||||
infoRef: xref.trailer.getRaw("Info") || null,
|
||||
info: infoObj,
|
||||
fileIds: xref.trailer.get("ID") || null,
|
||||
@ -669,20 +672,24 @@ class WorkerMessageHandler {
|
||||
filename,
|
||||
};
|
||||
}
|
||||
xref.resetNewRef();
|
||||
|
||||
return incrementalUpdate({
|
||||
originalData: stream.bytes,
|
||||
xrefInfo: newXrefInfo,
|
||||
newRefs,
|
||||
xref,
|
||||
hasXfa: !!xfa,
|
||||
xfaDatasetsRef,
|
||||
hasXfaDatasetsEntry,
|
||||
acroFormRef,
|
||||
acroForm,
|
||||
xfaData,
|
||||
});
|
||||
try {
|
||||
return incrementalUpdate({
|
||||
originalData: stream.bytes,
|
||||
xrefInfo: newXrefInfo,
|
||||
newRefs,
|
||||
xref,
|
||||
hasXfa: !!xfa,
|
||||
xfaDatasetsRef,
|
||||
hasXfaDatasetsEntry,
|
||||
needAppearances,
|
||||
acroFormRef,
|
||||
acroForm,
|
||||
xfaData,
|
||||
});
|
||||
} finally {
|
||||
xref.resetNewTemporaryRef();
|
||||
}
|
||||
});
|
||||
}
|
||||
);
|
||||
|
@ -46,7 +46,7 @@ function writeStream(stream, buffer, transform) {
|
||||
if (transform !== null) {
|
||||
string = transform.encryptString(string);
|
||||
}
|
||||
buffer.push(string, "\nendstream\n");
|
||||
buffer.push(string, "\nendstream");
|
||||
}
|
||||
|
||||
function writeArray(array, buffer, transform) {
|
||||
@ -150,54 +150,58 @@ function writeXFADataForAcroform(str, newRefs) {
|
||||
return buffer.join("");
|
||||
}
|
||||
|
||||
function updateXFA({
|
||||
xfaData,
|
||||
xfaDatasetsRef,
|
||||
hasXfaDatasetsEntry,
|
||||
acroFormRef,
|
||||
acroForm,
|
||||
newRefs,
|
||||
function updateAcroform({
|
||||
xref,
|
||||
xrefInfo,
|
||||
acroForm,
|
||||
acroFormRef,
|
||||
hasXfa,
|
||||
hasXfaDatasetsEntry,
|
||||
xfaDatasetsRef,
|
||||
needAppearances,
|
||||
newRefs,
|
||||
}) {
|
||||
if (xref === null) {
|
||||
if (hasXfa && !hasXfaDatasetsEntry && !xfaDatasetsRef) {
|
||||
warn("XFA - Cannot save it");
|
||||
}
|
||||
|
||||
if (!needAppearances && (!hasXfa || !xfaDatasetsRef)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasXfaDatasetsEntry) {
|
||||
if (!acroFormRef) {
|
||||
warn("XFA - Cannot save it");
|
||||
return;
|
||||
}
|
||||
// Clone the acroForm.
|
||||
const dict = new Dict(xref);
|
||||
for (const key of acroForm.getKeys()) {
|
||||
dict.set(key, acroForm.getRaw(key));
|
||||
}
|
||||
|
||||
if (hasXfa && !hasXfaDatasetsEntry) {
|
||||
// We've a XFA array which doesn't contain a datasets entry.
|
||||
// So we'll update the AcroForm dictionary to have an XFA containing
|
||||
// the datasets.
|
||||
const oldXfa = acroForm.get("XFA");
|
||||
const newXfa = oldXfa.slice();
|
||||
const newXfa = acroForm.get("XFA").slice();
|
||||
newXfa.splice(2, 0, "datasets");
|
||||
newXfa.splice(3, 0, xfaDatasetsRef);
|
||||
|
||||
acroForm.set("XFA", newXfa);
|
||||
|
||||
const encrypt = xref.encrypt;
|
||||
let transform = null;
|
||||
if (encrypt) {
|
||||
transform = encrypt.createCipherTransform(
|
||||
acroFormRef.num,
|
||||
acroFormRef.gen
|
||||
);
|
||||
}
|
||||
|
||||
const buffer = [`${acroFormRef.num} ${acroFormRef.gen} obj\n`];
|
||||
writeDict(acroForm, buffer, transform);
|
||||
buffer.push("\n");
|
||||
|
||||
acroForm.set("XFA", oldXfa);
|
||||
|
||||
newRefs.push({ ref: acroFormRef, data: buffer.join("") });
|
||||
dict.set("XFA", newXfa);
|
||||
}
|
||||
|
||||
if (needAppearances) {
|
||||
dict.set("NeedAppearances", true);
|
||||
}
|
||||
|
||||
const encrypt = xref.encrypt;
|
||||
let transform = null;
|
||||
if (encrypt) {
|
||||
transform = encrypt.createCipherTransform(acroFormRef.num, acroFormRef.gen);
|
||||
}
|
||||
|
||||
const buffer = [];
|
||||
writeObject(acroFormRef, dict, buffer, transform);
|
||||
|
||||
newRefs.push({ ref: acroFormRef, data: buffer.join("") });
|
||||
}
|
||||
|
||||
function updateXFA({ xfaData, xfaDatasetsRef, newRefs, xref }) {
|
||||
if (xfaData === null) {
|
||||
const datasets = xref.fetchIfRef(xfaDatasetsRef);
|
||||
xfaData = writeXFADataForAcroform(datasets.getString(), newRefs);
|
||||
@ -228,20 +232,28 @@ function incrementalUpdate({
|
||||
hasXfa = false,
|
||||
xfaDatasetsRef = null,
|
||||
hasXfaDatasetsEntry = false,
|
||||
needAppearances,
|
||||
acroFormRef = null,
|
||||
acroForm = null,
|
||||
xfaData = null,
|
||||
}) {
|
||||
updateAcroform({
|
||||
xref,
|
||||
acroForm,
|
||||
acroFormRef,
|
||||
hasXfa,
|
||||
hasXfaDatasetsEntry,
|
||||
xfaDatasetsRef,
|
||||
needAppearances,
|
||||
newRefs,
|
||||
});
|
||||
|
||||
if (hasXfa) {
|
||||
updateXFA({
|
||||
xfaData,
|
||||
xfaDatasetsRef,
|
||||
hasXfaDatasetsEntry,
|
||||
acroFormRef,
|
||||
acroForm,
|
||||
newRefs,
|
||||
xref,
|
||||
xrefInfo,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -42,18 +42,34 @@ class XRef {
|
||||
this._cacheMap = new Map(); // Prepare the XRef cache.
|
||||
this._pendingRefs = new RefSet();
|
||||
this.stats = new DocStats(pdfManager.msgHandler);
|
||||
this._newRefNum = null;
|
||||
this._newPersistentRefNum = null;
|
||||
this._newTemporaryRefNum = null;
|
||||
}
|
||||
|
||||
getNewRef() {
|
||||
if (this._newRefNum === null) {
|
||||
this._newRefNum = this.entries.length || 1;
|
||||
getNewPersistentRef(obj) {
|
||||
// When printing we don't care that much about the ref number by itself, it
|
||||
// can increase for ever and it allows to keep some re-usable refs.
|
||||
if (this._newPersistentRefNum === null) {
|
||||
this._newPersistentRefNum = this.entries.length || 1;
|
||||
}
|
||||
return Ref.get(this._newRefNum++, 0);
|
||||
const num = this._newPersistentRefNum++;
|
||||
this._cacheMap.set(num, obj);
|
||||
return Ref.get(num, 0);
|
||||
}
|
||||
|
||||
resetNewRef() {
|
||||
this._newRefNum = null;
|
||||
getNewTemporaryRef() {
|
||||
// When saving we want to have some minimal numbers.
|
||||
// Those refs are only created in order to be written in the final pdf
|
||||
// stream.
|
||||
if (this._newTemporaryRefNum === null) {
|
||||
this._newTemporaryRefNum = this.entries.length || 1;
|
||||
}
|
||||
return Ref.get(this._newTemporaryRefNum++, 0);
|
||||
}
|
||||
|
||||
resetNewTemporaryRef() {
|
||||
// Called once saving is finished.
|
||||
this._newTemporaryRefNum = null;
|
||||
}
|
||||
|
||||
setStartXRef(startXRef) {
|
||||
|
@ -2275,6 +2275,21 @@ class CanvasGraphics {
|
||||
|
||||
ctx.lineWidth = lineWidth;
|
||||
|
||||
if (font.isInvalidPDFjsFont) {
|
||||
const chars = [];
|
||||
let width = 0;
|
||||
for (const glyph of glyphs) {
|
||||
chars.push(glyph.unicode);
|
||||
width += glyph.width;
|
||||
}
|
||||
ctx.fillText(chars.join(""), 0, 0);
|
||||
current.x += width * widthAdvanceScale * textHScale;
|
||||
ctx.restore();
|
||||
this.compose();
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let x = 0,
|
||||
i;
|
||||
for (i = 0; i < glyphsLength; ++i) {
|
||||
|
@ -305,12 +305,7 @@ class FreeTextEditor extends AnnotationEditor {
|
||||
}
|
||||
const buffer = [];
|
||||
for (const div of divs) {
|
||||
const first = div.firstChild;
|
||||
if (first?.nodeName === "#text") {
|
||||
buffer.push(first.data);
|
||||
} else {
|
||||
buffer.push("");
|
||||
}
|
||||
buffer.push(div.innerText.replace(/\r\n?|\n/, ""));
|
||||
}
|
||||
return buffer.join("\n");
|
||||
}
|
||||
|
@ -30,6 +30,7 @@ const FONT_IDENTITY_MATRIX = [0.001, 0, 0, 0.001, 0, 0];
|
||||
// the font size. Acrobat seems to use this value.
|
||||
const LINE_FACTOR = 1.35;
|
||||
const LINE_DESCENT_FACTOR = 0.35;
|
||||
const BASELINE_FACTOR = LINE_DESCENT_FACTOR / LINE_FACTOR;
|
||||
|
||||
/**
|
||||
* Refer to the `WorkerTransport.getRenderingIntent`-method in the API, to see
|
||||
@ -47,6 +48,7 @@ const RenderingIntentFlag = {
|
||||
ANY: 0x01,
|
||||
DISPLAY: 0x02,
|
||||
PRINT: 0x04,
|
||||
SAVE: 0x08,
|
||||
ANNOTATIONS_FORMS: 0x10,
|
||||
ANNOTATIONS_STORAGE: 0x20,
|
||||
ANNOTATIONS_DISABLE: 0x40,
|
||||
@ -1159,6 +1161,7 @@ export {
|
||||
arraysToBytes,
|
||||
assert,
|
||||
BaseException,
|
||||
BASELINE_FACTOR,
|
||||
bytesToString,
|
||||
CMapCompressionType,
|
||||
createPromiseCapability,
|
||||
|
@ -479,7 +479,30 @@ class Driver {
|
||||
enableXfa: task.enableXfa,
|
||||
styleElement: xfaStyleElement,
|
||||
});
|
||||
loadingTask.promise.then(
|
||||
let promise = loadingTask.promise;
|
||||
|
||||
if (task.save) {
|
||||
if (!task.annotationStorage) {
|
||||
promise = Promise.reject(
|
||||
new Error("Missing `annotationStorage` entry.")
|
||||
);
|
||||
} else {
|
||||
promise = loadingTask.promise.then(async doc => {
|
||||
for (const [key, value] of Object.entries(
|
||||
task.annotationStorage
|
||||
)) {
|
||||
doc.annotationStorage.setValue(key, value);
|
||||
}
|
||||
const data = await doc.saveDocument();
|
||||
await loadingTask.destroy();
|
||||
delete task.annotationStorage;
|
||||
|
||||
return getDocument(data).promise;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
promise.then(
|
||||
async doc => {
|
||||
if (task.enableXfa) {
|
||||
task.fontRules = "";
|
||||
|
2
test/pdfs/.gitignore
vendored
2
test/pdfs/.gitignore
vendored
@ -551,3 +551,5 @@
|
||||
!bug1795263.pdf
|
||||
!issue15597.pdf
|
||||
!bug1796741.pdf
|
||||
!textfields.pdf
|
||||
!freetext_no_appearance.pdf
|
||||
|
BIN
test/pdfs/freetext_no_appearance.pdf
Executable file
BIN
test/pdfs/freetext_no_appearance.pdf
Executable file
Binary file not shown.
BIN
test/pdfs/textfields.pdf
Executable file
BIN
test/pdfs/textfields.pdf
Executable file
Binary file not shown.
@ -6973,5 +6973,225 @@
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true
|
||||
},
|
||||
{ "id": "ascii_print_textfields",
|
||||
"file": "pdfs/textfields.pdf",
|
||||
"md5": "5f743ca838ff9b7a286dbe52002860b7",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"32R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"35R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"38R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"34R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"37R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"40R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"33R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
},
|
||||
"36R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
},
|
||||
"39R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "arabic_print_textfields",
|
||||
"file": "pdfs/textfields.pdf",
|
||||
"md5": "5f743ca838ff9b7a286dbe52002860b7",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"32R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"35R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"38R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"34R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"37R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"40R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"33R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
},
|
||||
"36R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
},
|
||||
"39R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "ascii_save_print_textfields",
|
||||
"file": "pdfs/textfields.pdf",
|
||||
"md5": "5f743ca838ff9b7a286dbe52002860b7",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"save": true,
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"32R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"35R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"38R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"34R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"37R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"40R": {
|
||||
"value": "Hello World"
|
||||
},
|
||||
"33R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
},
|
||||
"36R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
},
|
||||
"39R": {
|
||||
"value": "Hello World\nDlrow Olleh\nHello World"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "arabic_save_print_textfields",
|
||||
"file": "pdfs/textfields.pdf",
|
||||
"md5": "5f743ca838ff9b7a286dbe52002860b7",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"save": true,
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"32R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"35R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"38R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"34R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"37R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"40R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
},
|
||||
"33R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
},
|
||||
"36R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
},
|
||||
"39R": {
|
||||
"value": "مرحبا بالعالم\nملاعلاب ابحرم\nمرحبا بالعالم"
|
||||
}
|
||||
}
|
||||
},
|
||||
{ "id": "freetext_no_appearance",
|
||||
"file": "pdfs/freetext_no_appearance.pdf",
|
||||
"md5": "1dc519c06f1dc6f6e594f168080dcde9",
|
||||
"rounds": 1,
|
||||
"type": "eq"
|
||||
},
|
||||
{ "id": "freetext_print_no_appearance",
|
||||
"file": "pdfs/freetext_no_appearance.pdf",
|
||||
"md5": "1dc519c06f1dc6f6e594f168080dcde9",
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true
|
||||
},
|
||||
{
|
||||
"id": "tracemonkey-multi-lang-editors",
|
||||
"file": "pdfs/tracemonkey.pdf",
|
||||
"md5": "9a192d8b1a7dc652a19835f6f08098bd",
|
||||
"rounds": 1,
|
||||
"lastPage": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"pdfjs_internal_editor_0": {
|
||||
"annotationType": 3,
|
||||
"color": [255, 0, 0],
|
||||
"fontSize": 10,
|
||||
"value": "Hello World",
|
||||
"pageIndex": 0,
|
||||
"rect": [67.5, 143, 119, 156.5],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_1": {
|
||||
"annotationType": 3,
|
||||
"color": [0, 255, 0],
|
||||
"fontSize": 10,
|
||||
"value": "مرحبا بالعالم",
|
||||
"pageIndex": 0,
|
||||
"rect": [67.5, 243, 119, 256.5],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_2": {
|
||||
"annotationType": 3,
|
||||
"color": [0, 0, 255],
|
||||
"fontSize": 10,
|
||||
"value": "你好世界",
|
||||
"pageIndex": 0,
|
||||
"rect": [67.5, 343, 119, 356.5],
|
||||
"rotation": 0
|
||||
},
|
||||
"pdfjs_internal_editor_3": {
|
||||
"annotationType": 3,
|
||||
"color": [255, 0, 255],
|
||||
"fontSize": 10,
|
||||
"value": "Hello World 你好世界 مرحبا بالعالم",
|
||||
"pageIndex": 0,
|
||||
"rect": [67.5, 443, 222, 456.5],
|
||||
"rotation": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "issue12233-arabic-print",
|
||||
"file": "pdfs/issue12233.pdf",
|
||||
"md5": "6099fc695fe018ce444752929d86f9c8",
|
||||
"link": true,
|
||||
"rounds": 1,
|
||||
"type": "eq",
|
||||
"print": true,
|
||||
"annotationStorage": {
|
||||
"23R": {
|
||||
"value": "مرحبا بالعالم"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -56,6 +56,9 @@ describe("annotation", function () {
|
||||
acroForm: new Dict(),
|
||||
},
|
||||
};
|
||||
this.evaluatorOptions = {
|
||||
isOffscreenCanvasSupported: false,
|
||||
};
|
||||
}
|
||||
|
||||
ensure(obj, prop, args) {
|
||||
@ -1611,11 +1614,12 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm" +
|
||||
" 2 3.04 Td (test\\\\print) Tj ET Q EMC"
|
||||
" 2 3.07 Td (test\\\\print) Tj ET Q EMC"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1645,13 +1649,14 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
const utf16String =
|
||||
"\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e";
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm" +
|
||||
` 2 2 Td (${utf16String}) Tj ET Q EMC`
|
||||
` 2 3.07 Td (${utf16String}) Tj ET Q EMC`
|
||||
);
|
||||
});
|
||||
|
||||
@ -1728,11 +1733,12 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5.92 Tf 0 g 1 0 0 1 0 0 Tm" +
|
||||
" 2 3.23 Td (test \\(print\\)) Tj ET Q EMC"
|
||||
" 2 3.07 Td (test \\(print\\)) Tj ET Q EMC"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1762,13 +1768,14 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
const utf16String =
|
||||
"\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f\x4e\x16\x75\x4c\x30\x6e";
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Goth 3.5 Tf 0 g 1 0 0 1 0 0 Tm" +
|
||||
` 2 2 Td (${utf16String}) Tj ET Q EMC`
|
||||
"/Tx BMC q BT /Goth 5.92 Tf 0 g 1 0 0 1 0 0 Tm" +
|
||||
` 2 3.07 Td (${utf16String}) Tj ET Q EMC`
|
||||
);
|
||||
});
|
||||
|
||||
@ -1795,6 +1802,7 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(null);
|
||||
@ -1827,17 +1835,18 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " +
|
||||
"2 -5 Td (a aa aaa ) Tj\n" +
|
||||
"0 -5 Td (aaaa aaaaa ) Tj\n" +
|
||||
"0 -5 Td (aaaaaa ) Tj\n" +
|
||||
"0 -5 Td (pneumonoultr) Tj\n" +
|
||||
"0 -5 Td (amicroscopi) Tj\n" +
|
||||
"0 -5 Td (csilicovolca) Tj\n" +
|
||||
"0 -5 Td (noconiosis) Tj ET Q EMC"
|
||||
"2 -6.93 Td (a aa aaa ) Tj\n" +
|
||||
"0 -8 Td (aaaa aaaaa ) Tj\n" +
|
||||
"0 -8 Td (aaaaaa ) Tj\n" +
|
||||
"0 -8 Td (pneumonoultr) Tj\n" +
|
||||
"0 -8 Td (amicroscopi) Tj\n" +
|
||||
"0 -8 Td (csilicovolca) Tj\n" +
|
||||
"0 -8 Td (noconiosis) Tj ET Q EMC"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1868,12 +1877,13 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 10 Tm " +
|
||||
"2 -5 Td (\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f) Tj\n" +
|
||||
"0 -5 Td (\x4e\x16\x75\x4c\x30\x6e) Tj ET Q EMC"
|
||||
"2 -6.93 Td (\x30\x53\x30\x93\x30\x6b\x30\x61\x30\x6f) Tj\n" +
|
||||
"0 -8 Td (\x4e\x16\x75\x4c\x30\x6e) Tj ET Q EMC"
|
||||
);
|
||||
});
|
||||
|
||||
@ -1890,25 +1900,25 @@ describe("annotation", function () {
|
||||
partialEvaluator.xref = xref;
|
||||
const expectedAppearance =
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 10 Tm " +
|
||||
"2 -5 Td " +
|
||||
"2 -6.93 Td " +
|
||||
"(Lorem ipsum dolor sit amet, consectetur adipiscing elit.) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(Aliquam vitae felis ac lectus bibendum ultricies quis non) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"( diam.) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(Morbi id porttitor quam, a iaculis dui.) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(Pellentesque habitant morbi tristique senectus et netus ) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(et malesuada fames ac turpis egestas.) Tj\n" +
|
||||
"0 -5 Td () Tj\n" +
|
||||
"0 -5 Td () Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td () Tj\n" +
|
||||
"0 -8 Td () Tj\n" +
|
||||
"0 -8 Td " +
|
||||
"(Nulla consectetur, ligula in tincidunt placerat, velit ) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(augue consectetur orci, sed mattis libero nunc ut massa.) Tj\n" +
|
||||
"0 -5 Td " +
|
||||
"0 -8 Td " +
|
||||
"(Etiam facilisis tempus interdum.) Tj ET Q EMC";
|
||||
|
||||
const annotation = await AnnotationFactory.create(
|
||||
@ -1933,8 +1943,10 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
|
||||
expect(appearance).toEqual(expectedAppearance);
|
||||
});
|
||||
|
||||
@ -1962,10 +1974,11 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 3.035 Tm" +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 2 3.07 Tm" +
|
||||
" (a) Tj 8 0 Td (a) Tj 8 0 Td (\\() Tj" +
|
||||
" 8 0 Td (a) Tj 8 0 Td (a) Tj" +
|
||||
" 8 0 Td (\\)) Tj 8 0 Td (a) Tj" +
|
||||
@ -2002,10 +2015,11 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 2 2 Tm" +
|
||||
"/Tx BMC q BT /Goth 5 Tf 1 0 0 1 2 3.07 Tm" +
|
||||
" (\x30\x53) Tj 8 0 Td (\x30\x93) Tj 8 0 Td (\x30\x6b) Tj" +
|
||||
" 8 0 Td (\x30\x61) Tj 8 0 Td (\x30\x6f) Tj" +
|
||||
" 8 0 Td (\x4e\x16) Tj 8 0 Td (\x75\x4c) Tj" +
|
||||
@ -2051,7 +2065,7 @@ describe("annotation", function () {
|
||||
expect(newData.data).toEqual(
|
||||
"2 0 obj\n<< /Length 74 /Subtype /Form /Resources " +
|
||||
"<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10]>> stream\n" +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.07 Td (hello world) Tj " +
|
||||
"ET Q EMC\nendstream\nendobj\n"
|
||||
);
|
||||
});
|
||||
@ -2092,12 +2106,12 @@ describe("annotation", function () {
|
||||
"123 0 obj\n" +
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Tx /DA (/Helv 5 Tf) /DR " +
|
||||
"<< /Font << /Helv 314 0 R>>>> /Rect [0 0 32 10] " +
|
||||
"/V (hello world) /AP << /N 2 0 R>> /M (date) /MK << /R 90>>>>\nendobj\n"
|
||||
"/V (hello world) /MK << /R 90>> /AP << /N 2 0 R>> /M (date)>>\nendobj\n"
|
||||
);
|
||||
expect(newData.data).toEqual(
|
||||
"2 0 obj\n<< /Length 74 /Subtype /Form /Resources " +
|
||||
"<< /Font << /Helv 314 0 R>>>> /BBox [0 0 32 10] /Matrix [0 1 -1 0 32 0]>> stream\n" +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 3.04 Td (hello world) Tj " +
|
||||
"/Tx BMC q BT /Helv 5 Tf 1 0 0 1 0 0 Tm 2 2.94 Td (hello world) Tj " +
|
||||
"ET Q EMC\nendstream\nendobj\n"
|
||||
);
|
||||
});
|
||||
@ -2226,9 +2240,9 @@ describe("annotation", function () {
|
||||
`/V (\xfe\xff${utf16String}) /AP << /N 2 0 R>> /M (date)>>\nendobj\n`
|
||||
);
|
||||
expect(newData.data).toEqual(
|
||||
"2 0 obj\n<< /Length 76 /Subtype /Form /Resources " +
|
||||
"2 0 obj\n<< /Length 79 /Subtype /Form /Resources " +
|
||||
"<< /Font << /Helv 314 0 R /Goth 159 0 R>>>> /BBox [0 0 32 10]>> stream\n" +
|
||||
`/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2 2 Td (${utf16String}) Tj ` +
|
||||
`/Tx BMC q BT /Goth 5 Tf 1 0 0 1 0 0 Tm 2 3.07 Td (${utf16String}) Tj ` +
|
||||
"ET Q EMC\nendstream\nendobj\n"
|
||||
);
|
||||
});
|
||||
@ -3457,6 +3471,7 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
@ -3501,6 +3516,7 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
@ -3549,6 +3565,7 @@ describe("annotation", function () {
|
||||
const appearance = await annotation._getAppearance(
|
||||
partialEvaluator,
|
||||
task,
|
||||
RenderingIntentFlag.PRINT,
|
||||
annotationStorage
|
||||
);
|
||||
expect(appearance).toEqual(
|
||||
@ -3605,7 +3622,7 @@ describe("annotation", function () {
|
||||
"<< /Type /Annot /Subtype /Widget /FT /Ch /DA (/Helv 5 Tf) /DR " +
|
||||
"<< /Font << /Helv 314 0 R>>>> " +
|
||||
"/Rect [0 0 32 10] /Opt [(A) (B) (C)] /V (C) " +
|
||||
"/AP << /N 2 0 R>> /M (date) /MK << /R 270>>>>\nendobj\n"
|
||||
"/MK << /R 270>> /AP << /N 2 0 R>> /M (date)>>\nendobj\n"
|
||||
);
|
||||
expect(newData.data).toEqual(
|
||||
[
|
||||
@ -4052,7 +4069,6 @@ describe("annotation", function () {
|
||||
"ET\n" +
|
||||
"Q\n" +
|
||||
"endstream\n" +
|
||||
"\n" +
|
||||
"endobj\n"
|
||||
);
|
||||
});
|
||||
@ -4245,7 +4261,6 @@ describe("annotation", function () {
|
||||
"922 923 924 925 926 927 c\n" +
|
||||
"S\n" +
|
||||
"endstream\n" +
|
||||
"\n" +
|
||||
"endobj\n"
|
||||
);
|
||||
});
|
||||
@ -4309,7 +4324,6 @@ describe("annotation", function () {
|
||||
"922 923 924 925 926 927 c\n" +
|
||||
"S\n" +
|
||||
"endstream\n" +
|
||||
"\n" +
|
||||
"endobj\n"
|
||||
);
|
||||
});
|
||||
|
@ -66,6 +66,9 @@ describe("document", function () {
|
||||
}
|
||||
return value;
|
||||
},
|
||||
get evaluatorOptions() {
|
||||
return { isOffscreenCanvasSupported: false };
|
||||
},
|
||||
};
|
||||
const pdfDocument = new PDFDocument(pdfManager, stream);
|
||||
pdfDocument.xref = xref;
|
||||
|
@ -78,7 +78,8 @@ class XRefMock {
|
||||
constructor(array) {
|
||||
this._map = Object.create(null);
|
||||
this.stats = new DocStats({ send: () => {} });
|
||||
this._newRefNum = null;
|
||||
this._newTemporaryRefNum = null;
|
||||
this._newPersistentRefNum = null;
|
||||
this.stream = new NullStream();
|
||||
|
||||
for (const key in array) {
|
||||
@ -87,15 +88,24 @@ class XRefMock {
|
||||
}
|
||||
}
|
||||
|
||||
getNewRef() {
|
||||
if (this._newRefNum === null) {
|
||||
this._newRefNum = Object.keys(this._map).length || 1;
|
||||
getNewPersistentRef(obj) {
|
||||
if (this._newPersistentRefNum === null) {
|
||||
this._newPersistentRefNum = Object.keys(this._map).length || 1;
|
||||
}
|
||||
return Ref.get(this._newRefNum++, 0);
|
||||
const ref = Ref.get(this._newPersistentRefNum++, 0);
|
||||
this._map[ref.toString()] = obj;
|
||||
return ref;
|
||||
}
|
||||
|
||||
resetNewRef() {
|
||||
this.newRef = null;
|
||||
getNewTemporaryRef() {
|
||||
if (this._newTemporaryRefNum === null) {
|
||||
this._newTemporaryRefNum = Object.keys(this._map).length || 1;
|
||||
}
|
||||
return Ref.get(this._newTemporaryRefNum++, 0);
|
||||
}
|
||||
|
||||
resetNewTemporaryRef() {
|
||||
this._newTemporaryRefNum = null;
|
||||
}
|
||||
|
||||
fetch(ref) {
|
||||
|
@ -128,7 +128,7 @@ describe("Writer", function () {
|
||||
"/E (\\(hello\\\\world\\)) /F [1.23 4.5 6] " +
|
||||
"/G << /H 123 /I << /Length 8>> stream\n" +
|
||||
"a stream\n" +
|
||||
"endstream\n>> /J true /K false " +
|
||||
"endstream>> /J true /K false " +
|
||||
"/NullArr [null 10] /NullVal null>>";
|
||||
|
||||
expect(buffer.join("")).toEqual(expected);
|
||||
@ -194,6 +194,7 @@ describe("Writer", function () {
|
||||
"\n" +
|
||||
"789 0 obj\n" +
|
||||
"<< /XFA [(preamble) 123 0 R (datasets) 101112 0 R (postamble) 456 0 R]>>\n" +
|
||||
"endobj\n" +
|
||||
"101112 0 obj\n" +
|
||||
"<< /Type /EmbeddedFile /Length 20>>\n" +
|
||||
"stream\n" +
|
||||
@ -202,11 +203,11 @@ describe("Writer", function () {
|
||||
"endobj\n" +
|
||||
"131415 0 obj\n" +
|
||||
"<< /Size 131416 /Prev 314 /Type /XRef /Index [0 1 789 1 101112 1 131415 1] /W [1 1 2] /Length 16>> stream\n" +
|
||||
"\u0000\u0001ÿÿ\u0001\u0001\u0000\u0000\u0001T\u0000\u0000\u0001²\u0000\u0000\n" +
|
||||
"\u0000\u0001ÿÿ\u0001\u0001\u0000\u0000\u0001[\u0000\u0000\u0001¹\u0000\u0000\n" +
|
||||
"endstream\n" +
|
||||
"endobj\n" +
|
||||
"startxref\n" +
|
||||
"178\n" +
|
||||
"185\n" +
|
||||
"%%EOF\n";
|
||||
|
||||
expect(data).toEqual(expected);
|
||||
|
Loading…
Reference in New Issue
Block a user