Compare commits

..

9 Commits

Author SHA1 Message Date
Jonas Jenwald
10844326c7
Merge pull request #21497 from Snuffleupagus/substring-tweaks
Tweak some `String.prototype.substring()` usage
2026-06-24 20:32:34 +02:00
Jonas Jenwald
eee03693a0
Merge pull request #21499 from Snuffleupagus/version-6.1
Bump library version to `6.1`
2026-06-24 19:47:37 +02:00
Jonas Jenwald
7414f6ed5a Bump library version to 6.1
See commit b168293c173b0b9befe462c0b254136cf038c3ef
2026-06-24 19:32:42 +02:00
Jonas Jenwald
5964e88be1
Merge pull request #21488 from Snuffleupagus/annotationGlobals-catalog
Include the `catalog` instance in the `annotationGlobals` data
2026-06-24 18:57:53 +02:00
Jonas Jenwald
6718c2924c Tweak some String.prototype.substring() usage
In a few spots the `indexEnd` parameter is explicitly set to the string-length, which is unnecessary since that's the default value if the parameter is omitted; note https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring#description

In the `XMLParserBase.prototype._resolveEntities` method the `substring` usage can be replaced with an updated (and cached) regular expression that directly finds numbers.
2026-06-24 18:52:46 +02:00
Jonas Jenwald
44e637a064 Remove explicit xref usage in the ScreenAnnotation.prototype.#renditionActions method
Rather than fetching "raw" dictionary-data and then manually resolving any references, we can simply use `Dict.prototype.get` and `Dict`-iteration to access the needed data *directly* instead.
2026-06-24 10:46:07 +02:00
Jonas Jenwald
15d93e1f34 Introduce a helper method, in the Annotation class, for determining the attachment fileId
This avoids duplication between the `FileAttachmentAnnotation` and `MediaAnnotation` classes, since they currently include essentially the same code for determining the attachment `fileId`.
2026-06-24 10:45:52 +02:00
Jonas Jenwald
8a2c112c20 Simplify the Annotation.prototype.setAppearance method a tiny bit
It's not necessary to check if the /AS entry exists first, and it can just be fetched directly, since in that case the existing "is stream"-check won't be true anyway.

Also, move the `appearance` field definition to the top of the class instead.
2026-06-24 10:42:38 +02:00
Jonas Jenwald
f07a106529 Include the catalog instance in the annotationGlobals data
The `FileAttachmentAnnotation` and `MediaAnnotation` code needs to (synchronously) access a `catalog` method, which leads to unnecessarily verbose code.
This can be avoided by including the `catalog` instance in the `annotationGlobals` data, which is safe since it already includes data that's fetched asynchronously from the `catalog` instance.
2026-06-24 10:42:38 +02:00
5 changed files with 63 additions and 66 deletions

View File

@ -1,5 +1,5 @@
{ {
"stableVersion": "6.0.227", "stableVersion": "6.0.227",
"baseVersion": "e6e06cf6b5307fe39e0de69c6c884f816b0cb21b", "baseVersion": "b168293c173b0b9befe462c0b254136cf038c3ef",
"versionPrefix": "6.0." "versionPrefix": "6.1."
} }

View File

@ -81,10 +81,6 @@ import { parseMarkedContentProps } from "./evaluator_utils.js";
import { StringStream } from "./stream.js"; import { StringStream } from "./stream.js";
import { XFAFactory } from "./xfa/factory.js"; import { XFAFactory } from "./xfa/factory.js";
/**
* @import { Catalog } from "./catalog.js";
*/
class AnnotationFactory { class AnnotationFactory {
static createGlobals(pdfManager) { static createGlobals(pdfManager) {
return Promise.all([ return Promise.all([
@ -108,6 +104,7 @@ class AnnotationFactory {
globalColorSpaceCache, globalColorSpaceCache,
]) => ({ ]) => ({
pdfManager, pdfManager,
catalog: pdfManager.pdfDocument.catalog,
acroForm: acroForm instanceof Dict ? acroForm : Dict.empty, acroForm: acroForm instanceof Dict ? acroForm : Dict.empty,
xfaDatasets, xfaDatasets,
structTreeRoot, structTreeRoot,
@ -690,6 +687,8 @@ function getTransformMatrix(rect, bbox, matrix) {
} }
class Annotation { class Annotation {
appearance = null;
_oc = undefined; _oc = undefined;
constructor(params) { constructor(params) {
@ -1178,8 +1177,6 @@ class Annotation {
* @param {Dict} dict - The annotation's data dictionary * @param {Dict} dict - The annotation's data dictionary
*/ */
setAppearance(dict) { setAppearance(dict) {
this.appearance = null;
const appearanceStates = dict.get("AP"); const appearanceStates = dict.get("AP");
if (!(appearanceStates instanceof Dict)) { if (!(appearanceStates instanceof Dict)) {
return; return;
@ -1198,7 +1195,7 @@ class Annotation {
// In case the normal appearance is a dictionary, the `AS` entry provides // In case the normal appearance is a dictionary, the `AS` entry provides
// the key of the stream in this dictionary. // the key of the stream in this dictionary.
const as = dict.get("AS"); const as = dict.get("AS");
if (!(as instanceof Name) || !normalAppearanceState.has(as.name)) { if (!(as instanceof Name)) {
return; return;
} }
const appearance = normalAppearanceState.get(as.name); const appearance = normalAppearanceState.get(as.name);
@ -1505,6 +1502,25 @@ class Annotation {
return fieldName.join("."); return fieldName.join(".");
} }
/**
* Encode the embedded content's reference in the id so it can be
* re-fetched from the xref on demand (see `Catalog.attachmentContent`)
* instead of being cached where `cleanup` would wipe it. The file-spec is
* usually indirect; when it's inline its embedded-file stream still isn't
* (streams are always indirect), so fall back to that ref.
*/
_getAttachmentId(fsDict, fsRef, annotationGlobals) {
if (!(fsDict instanceof Dict)) {
return undefined;
}
if (!(fsRef instanceof Ref)) {
fsRef = FileSpec.pickPlatformItem(fsDict.get("EF"), /* raw = */ true);
}
return fsRef instanceof Ref
? annotationGlobals.catalog.getAttachmentIdForAnnotation(fsRef)
: undefined;
}
get width() { get width() {
return this.data.rect[2] - this.data.rect[0]; return this.data.rect[2] - this.data.rect[0];
} }
@ -3108,7 +3124,7 @@ class TextWidgetAnnotation extends WidgetAnnotation {
} }
if (startChunk < line.length) { if (startChunk < line.length) {
chunks.push(line.substring(startChunk, line.length)); chunks.push(line.substring(startChunk));
} }
return chunks; return chunks;
@ -5421,33 +5437,15 @@ class FileAttachmentAnnotation extends MarkupAnnotation {
const { annotationGlobals, dict } = params; const { annotationGlobals, dict } = params;
const fsDict = dict.get("FS"); const fsDict = dict.get("FS");
const file = new FileSpec(fsDict);
/** @type {{catalog?: Catalog}} */
const { catalog } = annotationGlobals.pdfManager.pdfDocument;
// Encode the embedded content's reference in the id so it can be
// re-fetched from the xref on demand (see `Catalog.attachmentContent`)
// instead of being cached where `cleanup` would wipe it. The file-spec is
// usually indirect; when it's inline its embedded-file stream still isn't
// (streams are always indirect), so fall back to that ref.
let fileId;
if (fsDict instanceof Dict) {
let contentRef = dict.getRaw("FS");
if (!(contentRef instanceof Ref)) {
contentRef = FileSpec.pickPlatformItem(
fsDict.get("EF"),
/* raw = */ true
);
}
if (contentRef instanceof Ref) {
fileId = catalog?.getAttachmentIdForAnnotation(contentRef);
}
}
this.data.hasOwnCanvas = this.data.noRotate; this.data.hasOwnCanvas = this.data.noRotate;
this.data.noHTML = false; this.data.noHTML = false;
this.data.fileId = fileId; this.data.fileId = this._getAttachmentId(
this.data.file = file.serializable; fsDict,
dict.getRaw("FS"),
annotationGlobals
);
this.data.file = new FileSpec(fsDict).serializable;
const name = dict.get("Name"); const name = dict.get("Name");
this.data.name = this.data.name =
@ -5490,23 +5488,18 @@ class MediaAnnotation extends Annotation {
* when `assetRef` isn't itself a reference. * when `assetRef` isn't itself a reference.
* @param {string} asset.filename * @param {string} asset.filename
* @param {string} asset.contentType * @param {string} asset.contentType
* @param {Catalog} [catalog] * @param {Object} annotationGlobals
*/ */
_setMediaData({ assetRef, assetDict, filename, contentType }, catalog) { _setMediaData(
let contentRef = assetRef; { assetRef, assetDict, filename, contentType },
if (!(contentRef instanceof Ref)) { annotationGlobals
contentRef = FileSpec.pickPlatformItem( ) {
assetDict.get("EF"),
/* raw = */ true
);
}
const fileId =
contentRef instanceof Ref
? catalog?.getAttachmentIdForAnnotation(contentRef)
: undefined;
this.data.noHTML = false; this.data.noHTML = false;
this.data.richMedia = { fileId, filename, contentType }; this.data.richMedia = {
fileId: this._getAttachmentId(assetDict, assetRef, annotationGlobals),
filename,
contentType,
};
} }
/** /**
@ -5577,8 +5570,6 @@ class RichMediaAnnotation extends MediaAnnotation {
super(params); super(params);
const { dict, xref, annotationGlobals } = params; const { dict, xref, annotationGlobals } = params;
/** @type {{catalog?: Catalog}} */
const { catalog } = annotationGlobals.pdfManager.pdfDocument;
const content = dict.get("RichMediaContent"); const content = dict.get("RichMediaContent");
if (!(content instanceof Dict)) { if (!(content instanceof Dict)) {
@ -5590,8 +5581,7 @@ class RichMediaAnnotation extends MediaAnnotation {
warn("RichMedia annotation has no playable asset."); warn("RichMedia annotation has no playable asset.");
return; return;
} }
this._setMediaData(asset, annotationGlobals);
this._setMediaData(asset, catalog);
} }
/** /**
@ -5675,7 +5665,7 @@ class ScreenAnnotation extends MediaAnnotation {
// a /Movie); such ones simply render their appearance, so don't warn. // a /Movie); such ones simply render their appearance, so don't warn.
return; return;
} }
this._setMediaData(asset, annotationGlobals.pdfManager.pdfDocument.catalog); this._setMediaData(asset, annotationGlobals);
} }
/** /**
@ -5696,7 +5686,7 @@ class ScreenAnnotation extends MediaAnnotation {
* } | null} * } | null}
*/ */
static #findAsset(dict, xref) { static #findAsset(dict, xref) {
for (const action of this.#renditionActions(dict, xref)) { for (const action of this.#renditionActions(dict)) {
const asset = this.#findRenditionAsset( const asset = this.#findRenditionAsset(
action.get("R"), action.get("R"),
xref, xref,
@ -5709,10 +5699,10 @@ class ScreenAnnotation extends MediaAnnotation {
return null; return null;
} }
static *#renditionActions(dict, xref) { static *#renditionActions(dict) {
// The rendition action may be the activation action (/A) or one of the // The rendition action may be the activation action (/A) or one of the
// additional actions (/AA), e.g. page-open. // additional actions (/AA), e.g. page-open.
const action = xref.fetchIfRef(dict.getRaw("A")); const action = dict.get("A");
if ( if (
action instanceof Dict && action instanceof Dict &&
isName(action.get("S"), "Rendition") && isName(action.get("S"), "Rendition") &&
@ -5722,8 +5712,7 @@ class ScreenAnnotation extends MediaAnnotation {
} }
const additionalActions = dict.get("AA"); const additionalActions = dict.get("AA");
if (additionalActions instanceof Dict) { if (additionalActions instanceof Dict) {
for (const key of additionalActions.getKeys()) { for (const [, aa] of additionalActions) {
const aa = xref.fetchIfRef(additionalActions.getRaw(key));
if ( if (
aa instanceof Dict && aa instanceof Dict &&
isName(aa.get("S"), "Rendition") && isName(aa.get("S"), "Rendition") &&

View File

@ -386,7 +386,7 @@ function escapePDFName(str) {
} }
if (start < str.length) { if (start < str.length) {
buffer.push(str.substring(start, str.length)); buffer.push(str.substring(start));
} }
return buffer.join(""); return buffer.join("");
@ -545,7 +545,7 @@ function encodeToXmlString(str) {
return str; return str;
} }
if (start < str.length) { if (start < str.length) {
buffer.push(str.substring(start, str.length)); buffer.push(str.substring(start));
} }
return buffer.join(""); return buffer.join("");
} }

View File

@ -17,6 +17,7 @@
// https://github.com/mozilla/shumway/blob/16451d8836fa85f4b16eeda8b4bda2fa9e2b22b0/src/avm2/natives/xml.ts // https://github.com/mozilla/shumway/blob/16451d8836fa85f4b16eeda8b4bda2fa9e2b22b0/src/avm2/natives/xml.ts
import { encodeToXmlString } from "./core_utils.js"; import { encodeToXmlString } from "./core_utils.js";
import { shadow } from "../shared/util.js";
const XMLParserErrorCode = { const XMLParserErrorCode = {
NoError: 0, NoError: 0,
@ -47,12 +48,17 @@ function isWhitespaceString(s) {
} }
class XMLParserBase { class XMLParserBase {
static get _entityRegex() {
return shadow(this, "_entityRegex", /&(?:#x([^;]+)|#([^;]+)|([^;]+));/g);
}
_resolveEntities(s) { _resolveEntities(s) {
return s.replaceAll(/&([^;]+);/g, (all, entity) => { return s.replaceAll(XMLParserBase._entityRegex, (_, hex, dec, entity) => {
if (entity.substring(0, 2) === "#x") { if (hex) {
return String.fromCodePoint(parseInt(entity.substring(2), 16)); return String.fromCodePoint(parseInt(hex, 16));
} else if (entity.at(0) === "#") { }
return String.fromCodePoint(parseInt(entity.substring(1), 10)); if (dec) {
return String.fromCodePoint(parseInt(dec, 10));
} }
switch (entity) { switch (entity) {
case "lt": case "lt":

View File

@ -73,6 +73,8 @@ describe("document", function () {
const pdfDocument = new PDFDocument(pdfManager, stream); const pdfDocument = new PDFDocument(pdfManager, stream);
pdfDocument.xref = xref; pdfDocument.xref = xref;
pdfDocument.catalog = catalog; pdfDocument.catalog = catalog;
pdfManager.pdfDocument = pdfDocument;
return pdfDocument; return pdfDocument;
} }