diff --git a/src/core/annotation.js b/src/core/annotation.js index f7ce597e0..6761df712 100644 --- a/src/core/annotation.js +++ b/src/core/annotation.js @@ -287,6 +287,9 @@ class AnnotationFactory { case "FileAttachment": return new FileAttachmentAnnotation(parameters); + case "RichMedia": + return new RichMediaAnnotation(parameters); + default: if (!collectFields) { if (!subtype) { @@ -5454,6 +5457,162 @@ class FileAttachmentAnnotation extends MarkupAnnotation { } } +class RichMediaAnnotation extends Annotation { + constructor(params) { + super(params); + + this.data.noHTML = true; + + const { dict, xref, annotationGlobals } = params; + + const content = dict.get("RichMediaContent"); + if (!(content instanceof Dict)) { + return; + } + + const asset = RichMediaAnnotation.#findAsset(content, xref); + if (!asset) { + warn("RichMedia annotation has no playable asset."); + return; + } + const { assetRef, assetDict, filename, contentType } = asset; + + let contentRef = assetRef; + if (!(contentRef instanceof Ref)) { + contentRef = FileSpec.pickPlatformItem( + assetDict.get("EF"), + /* raw = */ true + ); + } + const fileId = + contentRef instanceof Ref + ? annotationGlobals.pdfManager.pdfDocument.catalog?.getAttachmentIdForAnnotation( + contentRef + ) + : undefined; + + this.data.noHTML = false; + this.data.richMedia = { fileId, filename, contentType }; + } + + /** + * Locate the primary playable embedded media asset. + * + * Per the spec (ISO 32000-2, 13.7), the asset to play is selected through + * `Configurations -> Instances -> Asset`. We pick the first instance with a + * natively playable asset rather than honoring the default configuration + * indicated by `RichMediaSettings`/activation; this keeps selection simple + * and matches the common single-configuration case. The `/Assets` name tree + * merely enumerates every embedded file; we don't use it as a fallback, since + * Acrobat itself won't play media that's only reachable that way. Flash + * instances are skipped, since they can't be played natively. + * + * @returns {{ + * assetRef: Ref | null, + * assetDict: Dict, + * filename: string, + * contentType: string, + * } | null} + */ + static #findAsset(content, xref) { + const configurations = content.get("Configurations"); + if (!Array.isArray(configurations)) { + return null; + } + for (const configRef of configurations) { + const config = xref.fetchIfRef(configRef); + if (!(config instanceof Dict)) { + continue; + } + const instances = config.get("Instances"); + if (!Array.isArray(instances)) { + continue; + } + for (const instanceRef of instances) { + const instance = xref.fetchIfRef(instanceRef); + if (!(instance instanceof Dict)) { + continue; + } + // Skip Flash instances: it's obsolete. + if (isName(instance.get("Subtype"), "Flash")) { + // Flash has been supported (see PDF 1.7 Extension Level 3). + continue; + } + const rawAsset = instance.getRaw("Asset"); + const asset = xref.fetchIfRef(rawAsset); + if (!(asset instanceof Dict)) { + continue; + } + const { filename } = new FileSpec(asset).serializable; + const contentType = RichMediaAnnotation.#getContentType( + asset, + filename + ); + if (!contentType) { + continue; + } + return { + assetRef: rawAsset instanceof Ref ? rawAsset : null, + assetDict: asset, + filename, + contentType, + }; + } + } + + return null; + } + + /** + * Determine the MIME type used to build the `