はじめに
「Wordで作ったチラシをInstagramに載せたい」「Wordを持っていない相手に、スマホで見られる形で送りたい」。こういうとき、Wordの文書を画像にしたくなります。ところがWordには、文書をJPGで保存する機能がありません。スクリーンショットで済ませるか、オンラインの変換サービスにアップロードするのが一般的です。
そこで、Wordファイルを一切サーバーに送らず、ブラウザの中だけで各ページをJPG・PNGにするツールを作りました。
この記事では、その仕組みと、実装で気をつけたところを紹介します。
構成
-
docx-preview:.docx を HTML のページ(
section.docx)として描画する - html2canvas-pro:描画したページを canvas に写し取る
- canvas.toBlob():JPG・PNG にする
- JSZip:複数ページを ZIP にまとめる
静的サイト(Next.js の output: "export"、Cloudflare Pages)なので、サーバー側の処理はありません。重いライブラリは、ファイルを選んだときに import() で読み込んでいます。
1. docx-preview で .docx をページごとの HTML にする
docx-preview の renderAsync() は、.docx を読み込んで、ページサイズと余白を反映した <section class="docx"> を並べてくれます。
const { renderAsync } = await import("docx-preview");
await renderAsync(file, container, container, {
className: "docx",
inWrapper: false,
breakPages: true,
ignoreLastRenderedPageBreak: false, // Wordが保存した改ページ位置を使う
renderHeaders: true,
renderFooters: true,
renderFootnotes: true,
useBase64URL: true, // 画像を data: URL にして、canvas に写せるようにする
});
ポイントは2つです。
-
ignoreLastRenderedPageBreak: false:Wordは保存するとき、最後に表示したときの改ページ位置(lastRenderedPageBreak)をファイルに書き込みます。これを使うと、Wordで見たときとほぼ同じ位置でページが分かれます。 -
useBase64URL: true:画像を data: URL で埋め込みます。canvas に写し取るときに、画像の読み込みで失敗しにくくなります。
Word内のリンクは安全なものだけ残す
.docx の中のリンクは、ファイルを作った人が自由に書けます。javascript: のリンクがそのまま <a href> になると危ないので、描画後に http(s):・mailto:・# 以外の href を消しています。
container.querySelectorAll("a[href]").forEach((a) => {
const href = a.getAttribute("href") ?? "";
if (!/^(https?:|mailto:|#)/i.test(href.trim())) a.removeAttribute("href");
});
2. プレビュー用と撮影用、2つ描画する
画面のプレビューは、スマホでもページ全体が見えるように CSS の zoom で縮小しています。ところが、縮小した要素をそのまま canvas に写すと、縮小後のサイズで撮影されてしまいます。
そこで、同じ文書をもう1つ、原寸で画面外に描画し、撮影にはそちらを使っています。React の createPortal で <body> 直下に置き、ページのレイアウト(zoom)の影響を受けないようにしています。
#docx-print-root {
position: absolute;
left: -100000px;
top: 0;
}
display: none にすると要素の大きさが 0 になって撮影できないので、画面の外に出すだけにしています。
3. html2canvas-pro でページを画像にする
各 section.docx を html2canvas で canvas に写します。CSS のピクセルは 96dpi なので、選んだ解像度(72・150・300dpi)を 96 で割った値を scale にします。
const { default: html2canvas } = await import("html2canvas-pro");
const scale = Math.min(dpi / 96, Math.sqrt(MAX_PIXELS / (width * height)));
const canvas = await html2canvas(section, { scale, backgroundColor: "#ffffff", logging: false, useCORS: true });
A4のページなら、150dpi で約 1240 × 1754 ピクセル、300dpi で約 2480 × 3508 ピクセルになります。
なぜ html2canvas-pro なのか
本家の html2canvas は、oklch() などの新しい CSS の色関数に対応していません。このサイトは Tailwind CSS v4 を使っていて、Tailwind v4 の色は oklch() で定義されているため、本家ではエラーになりました。これらの色関数に対応したフォークの html2canvas-pro を使っています。
iPhone のための上限
iPhone の Safari は、大きすぎる canvas を作ると失敗します(真っ白になる、エラーになる)。そこで、1枚あたり 1200万ピクセルを上限にして、それを超える場合は scale を下げています。A3 を 300dpi で変換するような場合だけ、少し小さめの画像になります。
また、使い終わった canvas はすぐに幅と高さを 0 にして、メモリを解放しています。ページ数の多い文書でも、スマホで落ちにくくなります。
function freeCanvas(canvas: HTMLCanvasElement) {
canvas.width = 0;
canvas.height = 0;
}
4. ページからはみ出した分は、ページの高さで切る
docx-preview の改ページは、Wordが保存した改ページ位置と明示的な改ページに基づいています。端末にWordと同じフォントがないと文字の幅が変わり、1つの section がページの高さより長くなることがあります。
そのため、撮影した canvas をページの高さ(section の min-height)ごとに切り分けて、それぞれを1枚の画像にしています。
const slicePx = Math.round(pageHeightPx * scale);
for (let y = 0; y < canvas.height; y += slicePx) {
const h = Math.min(slicePx, canvas.height - y);
if (h < slicePx * 0.02) break; // 端数で生じたわずかな余りは無視
const slice = document.createElement("canvas");
slice.width = canvas.width;
slice.height = slicePx;
const ctx = slice.getContext("2d")!;
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, slice.width, slice.height);
ctx.drawImage(canvas, 0, y, canvas.width, h, 0, 0, canvas.width, h);
slices.push(slice);
}
5. JPG・PNG にして、ZIP にまとめる
canvas は toBlob() で JPG(品質 0.85 または 0.95)か PNG にします。複数ページのときは JSZip でまとめますが、JPG・PNG はすでに圧縮されているので、ZIP は無圧縮(STORE)にしています。圧縮しても小さくならず、時間だけかかるためです。
const zip = new JSZip();
for (const file of files) zip.file(file.name, file.blob);
const blob = await zip.generateAsync({ type: "blob", compression: "STORE" });
限界
- フォント:端末にないフォントは近いフォントで表示するため、改行位置や文字の間隔がWordと少し変わることがあります。変換前にプレビューで確認できるようにしています。
- 古い形式(.doc):docx-preview は .docx(Office Open XML)専用です。.doc はWordで .docx として保存し直してもらう必要があります。
- 画像の文字:当然ですが、画像にした文字は選択できません。文字を残したい場合は、同じ描画結果を使った「Word→PDF変換」を用意しています。
まとめ
- docx-preview で .docx をページごとの HTML にして、html2canvas-pro で画像にした
- 撮影用の文書は、縮小していない原寸のものを画面外に描画する
- Tailwind v4 の
oklch()があるなら、html2canvas ではなく html2canvas-pro - iPhone 対策に、canvas の大きさに上限を設けて、使い終わったらすぐ解放する
実際に動くものはこちらです。Wordファイルはアップロードされず、すべてブラウザの中で処理されます。
- Word→JPG変換:https://pdftoolsjapan.com/word-to-jpg
- Word→PDF変換:https://pdftoolsjapan.com/word-to-pdf
同じ仕組みで、PDF圧縮・結合・分割など30種類ほどのツールを作っています。
