0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

ブラウザだけでWord(.docx)をJPG・PNG画像にする:docx-preview と html2canvas-pro

0
Last updated at Posted at 2026-10-21

はじめに

「Wordで作ったチラシをInstagramに載せたい」「Wordを持っていない相手に、スマホで見られる形で送りたい」。こういうとき、Wordの文書を画像にしたくなります。ところがWordには、文書をJPGで保存する機能がありません。スクリーンショットで済ませるか、オンラインの変換サービスにアップロードするのが一般的です。

そこで、Wordファイルを一切サーバーに送らず、ブラウザの中だけで各ページをJPG・PNGにするツールを作りました。

image.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ファイルはアップロードされず、すべてブラウザの中で処理されます。

同じ仕組みで、PDF圧縮・結合・分割など30種類ほどのツールを作っています。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?