diff --git a/src/core/annotation.js b/src/core/annotation.js index 646ad3bfa..d8c390d22 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -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); } diff --git a/src/core/core_utils.js b/src/core/core_utils.js index 77e7f3df1..f8ba471ee 100644 --- a/src/core/core_utils.js +++ b/src/core/core_utils.js @@ -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, diff --git a/src/core/default_appearance.js b/src/core/default_appearance.js index ba51edec5..43858e9f5 100644 --- a/src/core/default_appearance.js +++ b/src/core/default_appearance.js @@ -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> +endcodespacerange +1 beginbfrange +<0000> <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, +}; diff --git a/src/core/fonts.js b/src/core/fonts.js index fc56ea055..8e11ab189 100644 --- a/src/core/fonts.js +++ b/src/core/fonts.js @@ -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; diff --git a/src/core/worker.js b/src/core/worker.js index 073fb2882..4e17925c0 100644 --- a/src/core/worker.js +++ b/src/core/worker.js @@ -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(); + } }); } ); diff --git a/src/core/writer.js b/src/core/writer.js index 9485537b2..1de23297b 100644 --- a/src/core/writer.js +++ b/src/core/writer.js @@ -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, }); } diff --git a/src/core/xref.js b/src/core/xref.js index 0f1e957ea..6f1190bd7 100644 --- a/src/core/xref.js +++ b/src/core/xref.js @@ -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) { diff --git a/src/display/canvas.js b/src/display/canvas.js index 772b3cbe4..278be8388 100644 --- a/src/display/canvas.js +++ b/src/display/canvas.js @@ -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) { diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 23a3e1625..fd7cc1f47 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -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"); } diff --git a/src/shared/util.js b/src/shared/util.js index 723d43129..e193268bc 100644 --- a/src/shared/util.js +++ b/src/shared/util.js @@ -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, diff --git a/test/driver.js b/test/driver.js index eb51d3909..f63c12abc 100644 --- a/test/driver.js +++ b/test/driver.js @@ -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 = ""; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index c0ef6527d..63427e3a5 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -551,3 +551,5 @@ !bug1795263.pdf !issue15597.pdf !bug1796741.pdf +!textfields.pdf +!freetext_no_appearance.pdf diff --git a/test/pdfs/freetext_no_appearance.pdf b/test/pdfs/freetext_no_appearance.pdf new file mode 100755 index 000000000..567000948 Binary files /dev/null and b/test/pdfs/freetext_no_appearance.pdf differ diff --git a/test/pdfs/textfields.pdf b/test/pdfs/textfields.pdf new file mode 100755 index 000000000..1690d1c6e Binary files /dev/null and b/test/pdfs/textfields.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 9a66ad1cf..3e0b85e6b 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -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": "مرحبا بالعالم" + } + } } ] diff --git a/test/unit/annotation_spec.js b/test/unit/annotation_spec.js index dd9510626..0b5bf1726 100644 --- a/test/unit/annotation_spec.js +++ b/test/unit/annotation_spec.js @@ -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" ); }); diff --git a/test/unit/document_spec.js b/test/unit/document_spec.js index 077f0d0d0..94de57dfb 100644 --- a/test/unit/document_spec.js +++ b/test/unit/document_spec.js @@ -66,6 +66,9 @@ describe("document", function () { } return value; }, + get evaluatorOptions() { + return { isOffscreenCanvasSupported: false }; + }, }; const pdfDocument = new PDFDocument(pdfManager, stream); pdfDocument.xref = xref; diff --git a/test/unit/test_utils.js b/test/unit/test_utils.js index c252b9c4e..0987ed32a 100644 --- a/test/unit/test_utils.js +++ b/test/unit/test_utils.js @@ -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) { diff --git a/test/unit/writer_spec.js b/test/unit/writer_spec.js index 39504b2e1..c201c6dfc 100644 --- a/test/unit/writer_spec.js +++ b/test/unit/writer_spec.js @@ -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);