Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"coordsize",
"datas",
"DCMI",
"DIFAT",
"dcmitype",
"dcterms",
"docsify",
Expand All @@ -43,6 +44,7 @@
"jszip",
"NUMPAGES",
"odttf",
"OLEDS",
"OMML",
"ondata",
"onfile",
Expand Down
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,37 @@ const doc = new Document({

In a page without a bundler, load `dist/macros.umd.cjs` after `dist/index.umd.cjs`. It adds the `docxMacros` global. See the [macros](https://docx.js.org/#/usage/macros) documentation.

# Embedded Files

Workbooks, documents, presentations and any other file, such as a sound, a video or a PDF, are embedded in a document as OLE objects, which open from it, with `docx/embedded-files`:

```ts
import * as fs from "fs";
import { Document, Paragraph } from "docx";
import { EmbeddedFileRun } from "docx/embedded-files";

const doc = new Document({
sections: [
{
children: [
new Paragraph({
children: [
new EmbeddedFileRun({
fileName: "budget.xlsx",
data: fs.readFileSync("./budget.xlsx"),
preview: { type: "png", data: fs.readFileSync("./budget.png") },
transformation: { width: 240, height: 120 },
}),
],
}),
],
},
],
});
```

In a page without a bundler, load `dist/embedded-files.umd.cjs` after `dist/index.umd.cjs`. It adds the `docxEmbeddedFiles` global. See the [embedded files](https://docx.js.org/#/usage/embedded-files) documentation.

# Playground

Experience `docx` in action through [Docx.js Editor][docxjs-editor-url], an interactive playground where you can code and preview the results in real-time.
Expand Down
Binary file added demo/assets/embedded-files/budget.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/assets/embedded-files/budget.xlsx
Binary file not shown.
Binary file added demo/assets/embedded-files/letter-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/assets/embedded-files/tone-icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added demo/assets/embedded-files/tone.wav
Binary file not shown.
63 changes: 63 additions & 0 deletions demo/embedded-files/embedded-files.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Files embedded in a document, which open from it: a workbook shown as its table, a Word document and a sound shown as icons

import * as fs from "fs";
import { Document, HeadingLevel, Packer, Paragraph } from "docx";
import { EmbeddedFileRun } from "docx/embedded-files";

// A Word document to embed, made with docx too
const letter = new Document({
sections: [
{
children: [
new Paragraph({ text: "Dear reader,", heading: HeadingLevel.HEADING_1 }),
new Paragraph("This letter is in the report."),
],
},
],
});

Packer.toBuffer(letter).then((letterData) => {
const doc = new Document({
sections: [
{
children: [
new Paragraph({ text: "Quarterly report", heading: HeadingLevel.HEADING_1 }),
new Paragraph("The budget, which opens in Excel when double-clicked:"),
new Paragraph({
children: [
new EmbeddedFileRun({
fileName: "budget.xlsx",
data: fs.readFileSync("./demo/assets/embedded-files/budget.xlsx"),
preview: { type: "png", data: fs.readFileSync("./demo/assets/embedded-files/budget.png") },
transformation: { width: 241, height: 111 },
altText: "Sales and costs for January to March",
}),
],
}),
new Paragraph("A letter, which opens in Word, and a sound, which Word for Windows plays:"),
new Paragraph({
children: [
new EmbeddedFileRun({
fileName: "letter.docx",
data: letterData,
preview: { type: "png", data: fs.readFileSync("./demo/assets/embedded-files/letter-icon.png") },
transformation: { width: 96, height: 96 },
displayAsIcon: true,
}),
new EmbeddedFileRun({
fileName: "tone.wav",
data: fs.readFileSync("./demo/assets/embedded-files/tone.wav"),
preview: { type: "png", data: fs.readFileSync("./demo/assets/embedded-files/tone-icon.png") },
transformation: { width: 96, height: 96 },
}),
],
}),
],
},
],
});

Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});
});
1 change: 1 addition & 0 deletions docs/_sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@
- [Math from LaTeX](usage/math-latex.md)
- [HTML](usage/html.md)
- [Macros](usage/macros.md)
- [Embedded Files](usage/embedded-files.md)
- [Comments](usage/comments.md)
- [Footnotes](usage/footnotes.md)
- [Endnotes](usage/endnotes.md)
Expand Down
10 changes: 8 additions & 2 deletions docs/live-examples/live-examples.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
// docx/html writes math with docx/math, so it is loaded after it
"docx/html": { file: "html", global: "docxHtml", after: ["docx/math"] },
"docx/macros": { file: "macros", global: "docxMacros" },
"docx/embedded-files": { file: "embedded-files", global: "docxEmbeddedFiles" },
};

// Packages from npm an example can import too, from jsDelivr at the version the docs are written for: where each is, and
Expand All @@ -46,8 +47,13 @@
},
};

// docx-preview draws these as empty space
const NOT_DRAWN = { "docx/shapes": "shapes", "docx/charts": "charts", "docx/watermarks": "watermarks" };
// docx-preview draws these as empty space, or, for embedded files, not at all
const NOT_DRAWN = {
"docx/shapes": "shapes",
"docx/charts": "charts",
"docx/watermarks": "watermarks",
"docx/embedded-files": "embedded files",
};

// The type of a document with macros, a .docm, which Word doesn't open as a .docx. A blob's type is in lower case
const DOCM = "application/vnd.ms-word.document.macroenabled.12";
Expand Down
151 changes: 151 additions & 0 deletions docs/usage/embedded-files.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
# Embedded Files

<!-- cspell:ignore docm xlsm pptm -->

A document can have files embedded in it: a workbook, another document, a presentation, or any other file, such as a sound, a video or a PDF. Each is an OLE object, which the document shows as a picture, and which opens when it is double-clicked, as a file embedded with Word's **Insert > Object > Create from File** does.

`EmbeddedFileRun` embeds a file. It comes with the `docx` package, from `docx/embedded-files`, and goes in a paragraph, as an `ImageRun` does:

```ts
import { Document, Packer, Paragraph } from "docx";
import { EmbeddedFileRun } from "docx/embedded-files";
```

In a page without a bundler, load `dist/embedded-files.umd.cjs` after `dist/index.umd.cjs` (or `dist/embedded-files.iife.js` after `dist/index.iife.js`). It adds a `docxEmbeddedFiles` global, such as `new docxEmbeddedFiles.EmbeddedFileRun({ ... })`.

## Example

```ts live
import * as fs from "fs";
import { Document, Packer, Paragraph } from "docx";
import { EmbeddedFileRun } from "docx/embedded-files";

const doc = new Document({
sections: [
{
children: [
new Paragraph("The budget, which opens in Excel when it is double-clicked:"),
new Paragraph({
children: [
new EmbeddedFileRun({
fileName: "budget.xlsx",
data: fs.readFileSync("./demo/assets/embedded-files/budget.xlsx"),
preview: { type: "png", data: fs.readFileSync("./demo/assets/embedded-files/budget.png") },
transformation: { width: 241, height: 111 },
}),
],
}),
],
},
],
});

Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});
```

## Options

| Property | Type | Notes | Description |
| ---------------- | ----------------------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| `fileName` | `string` | Required | The file's name, such as `budget.xlsx`. Its extension says how the file is embedded, below |
| `data` | `Buffer \| string \| Uint8Array \| ArrayBuffer` | Required | The file, or a base64 data URI of it |
| `preview` | `{ type, data }` | Required | The picture the document shows for the file: a `"png"`, `"jpg"`, `"gif"` or `"bmp"`, as `type`, and its `data`, as an `ImageRun`'s |
| `transformation` | `{ width, height }` | Required | The size the picture is drawn at, in pixels, as an `ImageRun`'s |
| `displayAsIcon` | `boolean` | Optional | Shows the file as an icon, as Word's **Display as icon** does, rather than as its content. Default is `false` |
| `altText` | `string` | Optional | A description of the file for screen readers, as Word's **Alt Text** |

## How each file is embedded

An Excel workbook, a Word document or a PowerPoint presentation is embedded as itself, as Word embeds it, so Office opens it from the document, and changes made to it are saved in the document:

| Extension | Opens in | Its OLE ProgID |
| ---------------- | ---------- | ------------------------------------------------------ |
| `.xlsx`, `.xlsm` | Excel | `Excel.Sheet.12`, `Excel.SheetMacroEnabled.12` |
| `.docx`, `.docm` | Word | `Word.Document.12`, `Word.DocumentMacroEnabled.12` |
| `.pptx`, `.pptm` | PowerPoint | `PowerPoint.Show.12`, `PowerPoint.ShowMacroEnabled.12` |

`EmbeddedFileRun` throws for one of these that isn't a zip file, as Office's files are, such as an older Excel workbook (`.xls`) named `.xlsx`, as Office couldn't open it.

Any other file, such as a sound (`.mp3`, `.wav`), a video (`.mp4`), a PDF, or an older Office file (`.xls`, `.doc`, `.ppt`), is embedded in an OLE package (the `Package` ProgID), as Word for Windows embeds a file no application on the computer embeds itself. Word for Windows opens it with the application Windows opens its type with, after asking whether to. Word for Mac and LibreOffice show its picture and keep it in the document, but Word for Mac can't open it: it says the application can't be found. A packaged file is always shown as an icon.

```ts live
import * as fs from "fs";
import { Document, Packer, Paragraph } from "docx";
import { EmbeddedFileRun } from "docx/embedded-files";

const doc = new Document({
sections: [
{
children: [
new Paragraph("A sound, which Word for Windows plays when it is double-clicked:"),
new Paragraph({
children: [
new EmbeddedFileRun({
fileName: "tone.wav",
data: fs.readFileSync("./demo/assets/embedded-files/tone.wav"),
preview: { type: "png", data: fs.readFileSync("./demo/assets/embedded-files/tone-icon.png") },
transformation: { width: 96, height: 96 },
altText: "A one-second chime",
}),
],
}),
],
},
],
});

Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});
```

## The picture

docx doesn't draw a file's picture: give it one in `preview`, such as a screenshot of a workbook's table, or an icon with the file's name, as Word shows a file with **Display as icon**. Word shows the picture until the file is opened and changed in its application, which then draws the file again: its content, or, with `displayAsIcon`, its application's icon.

```ts live
import * as fs from "fs";
import { Document, Packer, Paragraph } from "docx";
import { EmbeddedFileRun } from "docx/embedded-files";

// A Word document to embed, made with docx too
const letter = new Document({ sections: [{ children: [new Paragraph("Dear reader, this letter is in the report.")] }] });
const letterData = await Packer.toBuffer(letter);

const doc = new Document({
sections: [
{
children: [
new Paragraph("A letter, which opens in Word when it is double-clicked:"),
new Paragraph({
children: [
new EmbeddedFileRun({
fileName: "letter.docx",
data: letterData,
preview: { type: "png", data: fs.readFileSync("./demo/assets/embedded-files/letter-icon.png") },
transformation: { width: 96, height: 96 },
displayAsIcon: true,
}),
],
}),
],
},
],
});

Packer.toBuffer(doc).then((buffer) => {
fs.writeFileSync("My Document.docx", buffer);
});
```

An embedded file goes anywhere a run does: in a table, a header, a footer, a footnote or a comment, and in a document `patchDocument` fills in.

## Examples

### A workbook, a document and a sound

[Example](https://raw.githubusercontent.com/dolanmiu/docx/master/demo/embedded-files/embedded-files.ts ":include")

_Source: https://github.com/dolanmiu/docx/blob/master/demo/embedded-files/embedded-files.ts_
11 changes: 8 additions & 3 deletions eslint.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -398,7 +398,8 @@ const config: Linter.Config<Linter.RulesRecord>[] = [
// internals, such as the Formatter. docx/math (src/math) re-exports docx's math, which docx still exports and keeps in
// src/file/paragraph/math. src/text-layout measures text for docx/shapes and docx/layout, and each bundles a copy.
// docx/html (src/html) imports docx/math by name too, for the math in HTML. docx/macros (src/macros) writes VBA
// projects for docx's macros
// projects for docx's macros, and docx/embedded-files (src/embedded-files) embeds files as OLE objects.
// src/compound-file writes the compound files both keep them in, and each bundles a copy
{
files: ["src/**/*.ts"],
ignores: [
Expand All @@ -409,6 +410,8 @@ const config: Linter.Config<Linter.RulesRecord>[] = [
"src/layout/**",
"src/html/**",
"src/macros/**",
"src/embedded-files/**",
"src/compound-file/**",
"src/text-layout/**",
"**/*.spec.ts",
],
Expand All @@ -420,7 +423,7 @@ const config: Linter.Config<Linter.RulesRecord>[] = [
patterns: [
{ group: ["docx/*"], message: "docx doesn't import its optional entries, such as docx/shapes" },
{
regex: "(^|/)(shapes|watermarks|charts|layout|html|macros|text-layout)(/|$)",
regex: "(^|/)(shapes|watermarks|charts|layout|html|macros|embedded-files|compound-file|text-layout)(/|$)",
message: "docx doesn't import its optional entries, such as docx/shapes",
},
],
Expand All @@ -437,6 +440,8 @@ const config: Linter.Config<Linter.RulesRecord>[] = [
"src/layout/*.ts",
"src/html/*.ts",
"src/macros/*.ts",
"src/embedded-files/*.ts",
"src/compound-file/*.ts",
"src/text-layout/*.ts",
],
ignores: ["**/*.spec.ts"],
Expand All @@ -449,7 +454,7 @@ const config: Linter.Config<Linter.RulesRecord>[] = [
group: ["@file/*", "@util/*", "@export/*", "@shared", "@shared/*"],
message: 'Optional entries import docx from "docx"',
},
{ regex: "^\\.\\./(?!text-layout(/|$))", message: 'Optional entries import docx from "docx"' },
{ regex: "^\\.\\./(?!(text-layout|compound-file)(/|$))", message: 'Optional entries import docx from "docx"' },
],
},
],
Expand Down
17 changes: 15 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,16 @@
"types": "./dist/macros.d.cts",
"default": "./dist/macros.cjs"
}
},
"./embedded-files": {
"import": {
"types": "./dist/embedded-files.d.ts",
"default": "./dist/embedded-files.mjs"
},
"require": {
"types": "./dist/embedded-files.d.cts",
"default": "./dist/embedded-files.cjs"
}
}
},
"typesVersions": {
Expand All @@ -110,21 +120,24 @@
],
"macros": [
"./dist/macros.d.ts"
],
"embedded-files": [
"./dist/embedded-files.d.ts"
]
}
},
"files": [
"dist"
],
"scripts": {
"build": "tsc && vite build && vite build --config vite.entries.config.ts --mode shapes && vite build --config vite.entries.config.ts --mode watermarks && vite build --config vite.entries.config.ts --mode charts && vite build --config vite.entries.config.ts --mode math && vite build --config vite.entries.config.ts --mode layout && vite build --config vite.entries.config.ts --mode html && vite build --config vite.entries.config.ts --mode macros",
"build": "tsc && vite build && vite build --config vite.entries.config.ts --mode shapes && vite build --config vite.entries.config.ts --mode watermarks && vite build --config vite.entries.config.ts --mode charts && vite build --config vite.entries.config.ts --mode math && vite build --config vite.entries.config.ts --mode layout && vite build --config vite.entries.config.ts --mode html && vite build --config vite.entries.config.ts --mode macros && vite build --config vite.entries.config.ts --mode embedded-files",
"test": "vitest --ui --coverage",
"test:ci": "vitest run --coverage",
"prepublishOnly": "npm run build --omit=dev",
"lint": "eslint --config eslint.config.ts",
"predemo": "npm run build",
"demo": "tsx ./demo/index.ts",
"typedoc": "typedoc src/index.ts src/shapes/index.ts src/watermarks/index.ts src/charts/index.ts src/math/index.ts src/layout/index.ts src/html/index.ts src/macros/index.ts --tsconfig tsconfig.typedoc.json",
"typedoc": "typedoc src/index.ts src/shapes/index.ts src/watermarks/index.ts src/charts/index.ts src/math/index.ts src/layout/index.ts src/html/index.ts src/macros/index.ts src/embedded-files/index.ts --tsconfig tsconfig.typedoc.json",
"prettier": "prettier -l \"{src,scripts,demo,docs}/**/*.{ts,html,md}\"",
"prettier:fix": "npm run prettier -- --write",
"cspell": "cspell \"{src,demo,docs,scripts}/**/*.{ts,scss,html,md}\" && cspell \"./*.*\"",
Expand Down
Loading
Loading