UNPKG

docxml

Version:

TypeScript (component) library for building and parsing a DOCX file

300 lines (298 loc) 12.1 kB
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) { if (kind === "m") throw new TypeError("Private method is not writable"); if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it"); return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value; }; var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) { if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter"); if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it"); return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver); }; var _Image_meta; import { BinaryFile } from '../classes/BinaryFile.js'; import { Component, } from '../classes/Component.js'; import { FileMime, RelationshipType } from '../enums.js'; import { registerComponent } from '../utilities/components.js'; import { create } from '../utilities/dom.js'; import { extensionListUris } from '../utilities/drawingml-extensions.js'; import { createRandomId, createUniqueNumericIdentifier } from '../utilities/identifiers.js'; import { emu } from '../utilities/length.js'; import { getMimeTypeForUint8Array } from '../utilities/mime-types.js'; import { NamespaceUri, QNS } from '../utilities/namespaces.js'; import { evaluateXPathToFirstNode, evaluateXPathToNodes, evaluateXPathToNumber, evaluateXPathToString, } from '../utilities/xquery.js'; /** * A component that represents an image in your DOCX document. You can create a new image by * passing any promise to an `Uint8Array` into the `data` prop, eg. get it from your file system * or from a web request. */ export class Image extends Component { constructor(props, ...children) { super(props, ...children); _Image_meta.set(this, void 0); __classPrivateFieldSet(this, _Image_meta, { location: `word/media/${createRandomId('img')}`, mime: props.mime ? Promise.resolve(props.mime) : null, relationshipId: null, extensions: {}, }, "f"); if (props.dataExtensions) { const { svg } = props.dataExtensions; if (svg !== undefined) { __classPrivateFieldGet(this, _Image_meta, "f").extensions.svg = { location: `word/media/${createRandomId('svg')}`, relationshipId: null, }; } } } get meta() { const embedMeta = __classPrivateFieldGet(this, _Image_meta, "f"); const props = this.props; return { get location() { return embedMeta.location; }, get mime() { if (embedMeta.mime === null) { embedMeta.mime = new Promise((resolve) => { props.data.then((data) => { resolve(getMimeTypeForUint8Array(data)); }); }); } return embedMeta.mime; }, get relationshipId() { return embedMeta.relationshipId; }, get extensions() { return { get svg() { const { svg } = embedMeta.extensions; if (!svg) { return undefined; } return { get location() { return svg.location; }, get relationshipId() { return svg.relationshipId; }, }; }, }; }, }; } /** * An event hook with which this component can ensure that the correct relationship type is * recorded to the relationship XML. */ async ensureRelationship(relationships) { const { location, mime, extensions } = this.meta; __classPrivateFieldGet(this, _Image_meta, "f").relationshipId = relationships.add(RelationshipType.image, BinaryFile.fromData(this.props.data, location, await mime)); const { svg } = extensions; if (__classPrivateFieldGet(this, _Image_meta, "f").extensions.svg && svg && this.props.dataExtensions?.svg) { __classPrivateFieldGet(this, _Image_meta, "f").extensions.svg.relationshipId = relationships.add(RelationshipType.image, BinaryFile.fromData(new TextEncoder().encode(await this.props.dataExtensions.svg), svg.location, FileMime.svg)); } } /** * Creates an XML DOM node for this component instance. */ toNode(_ancestry) { if (!__classPrivateFieldGet(this, _Image_meta, "f").relationshipId) { throw new Error('Cannot serialize an image outside the context of an Document'); } let extensionList = null; const { svg } = this.meta.extensions; if (svg) { extensionList = create(` element ${QNS.a}extLst { element ${QNS.a}ext { attribute uri { $extLstUseLocalDpi }, element ${QNS.a14}useLocalDpi { attribute val { "0" } } }, element ${QNS.a}ext { attribute uri { $extLstSvg }, element ${QNS.asvg}svgBlip { attribute ${QNS.r}embed { $relationshipId } } } } `, { relationshipId: svg.relationshipId, extLstUseLocalDpi: extensionListUris.useLocalDpi, extLstSvg: extensionListUris.svg, }); } return create(` element ${QNS.w}drawing { element ${QNS.wp}inline { element ${QNS.wp}extent { attribute cx { $width }, attribute cy { $height } }, element ${QNS.wp}docPr { attribute id { $identifier }, attribute name { $name }, attribute descr { $desc } }, element ${QNS.wp}cNvGraphicFramePr { element ${QNS.a}graphicFrameLocks { attribute noChangeAspect { "1" } } }, (: nb: _Must_ be prefixed with "a" or MS Word will refuse to open :) element ${QNS.a}graphic { element ${QNS.a}graphicData { attribute uri { "${NamespaceUri.pic}"}, element ${QNS.pic}pic { element ${QNS.pic}nvPicPr { element ${QNS.pic}cNvPr { attribute id { $identifier }, attribute name { $name }, attribute descr { $desc } }, element ${QNS.pic}cNvPicPr {} }, element ${QNS.pic}blipFill { element ${QNS.a}blip { attribute ${QNS.r}embed { $relationshipId }, attribute cstate { "print" }, $extensionList }, element ${QNS.a}stretch { element ${QNS.a}fillRect {} } }, element ${QNS.pic}spPr { element ${QNS.a}xfrm { element ${QNS.a}off { attribute x { "0" }, attribute y { "0" } }, element ${QNS.a}ext { attribute cx { $width }, attribute cy { $height } } }, element ${QNS.a}prstGeom { attribute prst { "rect" }, element ${QNS.a}avLst {} } } } } } } } `, { identifier: createUniqueNumericIdentifier(), relationshipId: __classPrivateFieldGet(this, _Image_meta, "f").relationshipId, width: Math.round(this.props.width.emu), height: Math.round(this.props.height.emu), name: this.props.title || '', desc: this.props.alt || '', extensionList, }); } /** * Asserts whether or not a given XML node correlates with this component. */ static matchesNode(node) { return node.nodeName === 'w:drawing'; } /** * Instantiate this component from the XML in an existing DOCX file. */ static fromNode(node, { archive, relationships }) { // Important nodes const inlineNode = evaluateXPathToFirstNode(`./${QNS.wp}inline`, node); const picNode = evaluateXPathToFirstNode(`./${QNS.a}graphic/${QNS.a}graphicData/${QNS.pic}pic`, inlineNode); const title = evaluateXPathToString(`./${QNS.wp}docPr/@name/string()`, inlineNode); const width = emu(evaluateXPathToNumber(`./${QNS.wp}extent/@cx/number()`, inlineNode)); const height = emu(evaluateXPathToNumber(`./${QNS.wp}extent/@cy/number()`, inlineNode)); if (relationships === null) { // Our simplified images are always expected to reference a relationship ID throw new Error('Failed to load image. The image is referencing a relationship ID but RelationhipsXml is null in the context.'); } const blipNode = evaluateXPathToFirstNode(`${QNS.pic}blipFill/${QNS.a}blip`, picNode); if (blipNode === null) { throw new Error('Failed to load image. No blip found inside a blipFill.'); } const { main, svg } = extractDataFromBlipNode(archive, relationships, blipNode); const dataExtensions = {}; if (svg) { dataExtensions.svg = svg.data; } const image = new Image({ data: main.data, dataExtensions, title, width, height, }); __classPrivateFieldGet(image, _Image_meta, "f").location = main.location; if (svg) { const { svg: svgMeta } = __classPrivateFieldGet(image, _Image_meta, "f").extensions; // We are certain that if we pass `dataExtensions` with `svg` in it // `Image` construtor makes it so image.#meta.extensions has `svg` too. // eslint-disable-next-line @typescript-eslint/no-non-null-assertion svgMeta.location = svg.location; } return image; } } _Image_meta = new WeakMap(); Object.defineProperty(Image, "children", { enumerable: true, configurable: true, writable: true, value: [] }); Object.defineProperty(Image, "mixed", { enumerable: true, configurable: true, writable: true, value: false }); registerComponent(Image); function extractDataFromBlipNode(archive, relationships, blipNode) { const blipEmbedRel = evaluateXPathToString(`@${QNS.r}embed/string()`, blipNode); const location = relationships.getTarget(blipEmbedRel); const data = archive.readBinary(location); const allLocationsAndData = { main: { data, location, }, }; const blipextLst = evaluateXPathToNodes(`./extLst/*`, blipNode); blipextLst.forEach((node) => { if (node.nodeType !== 1) { return; } const element = node; const extensionUri = element.getAttribute('uri'); if (extensionUri === extensionListUris.svg) { const extensionRel = element.children[0].getAttributeNS(NamespaceUri.r, 'embed'); if (extensionRel === null) { throw new Error('Failed to load image SVG extension. SVG extension URI found in extLst but its node does not follow the known format.'); } const location = relationships.getTarget(extensionRel); const data = archive.readText(location); allLocationsAndData.svg = { location, data, }; return; } // Implement other similar blip extensions here // if (extensionUri === "some other rui") { } }); return allLocationsAndData; }