docxml
Version:
TypeScript (component) library for building and parsing a DOCX file
300 lines (298 loc) • 12.1 kB
JavaScript
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;
}