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?

PDFをCanvasに描画する前に画素数を制限する:A4・300 DPIで考える実装と検証

0
Posted at

PDFをJPEGにする処理でメモリ不足が起きたとき、JPEGの品質を下げるだけでは解決しないことがあります。品質が効くのはエンコード後のファイルサイズです。その前段で、描画先のCanvasを確保する必要があります。

ここではブラウザのPDF変換を実装する人向けに、描画前に決める画素数の上限と、ダウンロード用ファイルの容量制限を分けて考えます。A4の具体例から入り、Node.jsで確認できる小さな関数に落とします。

AI利用の開示:この記事はAIエージェントが起草・構成しました。掲載コードは実行テストと独立したコードレビューを通した英語版のものを再利用し、A4の数値は同じ関数で再計算しています。ブラウザの実メモリ使用量を測った記事ではありません。

A4を300 DPIにすると何画素になるか

今回のA4サンプルは595.2756 × 841.8898ポイントです。1インチを72ポイントとする通常のPDF座標では、DPIから描画倍率を求める式は dpi / 72 です。幅と高さの両方に倍率を掛け、整数画素へ切り下げると、次の値になります。

要求DPI 幅 × 高さ(px) 画素数 RGBA8の1バッファ(MiB)
150 1240 × 1753 2,173,720 8.29
300 2480 × 3507 8,697,360 33.18

DPIが2倍になると、丸める前の画素数は4倍です。整数への丸めを含む表でも約4倍になります。JPEG品質の指定は、この計算には登場しません。

RGBA8の列は「1画素4バイト」を1,048,576で割ったモデル値です。MDNのImageData.dataが説明する rgba-unorm8 を仮定しており、PDFの解析、フォント、画像キャッシュ、エンコーダーやブラウザ自体のメモリは含めていません。

長辺だけ・総画素数だけ、では足りない

横長のページは総画素数が少なくても一辺が長くなります。逆に、各辺の上限だけでは正方形に近いページの総画素数を抑えきれません。要求DPI、幅、高さ、面積の各条件から求めた倍率の最小値を使います。

raster-budget.mjs として保存します。

export function fitRaster(widthPt, heightPt, requestedDpi, {
  maxEdge = 4096,
  maxPixels = 8_000_000,
} = {}) {
  if (![widthPt, heightPt, requestedDpi].every(n => Number.isFinite(n) && n > 0)) {
    throw new RangeError('Dimensions and DPI must be positive finite numbers');
  }
  if (![maxEdge, maxPixels].every(n => Number.isSafeInteger(n) && n > 0)) {
    throw new RangeError('Budgets must be positive safe integers');
  }
  const scale = Math.min(
    requestedDpi / 72,
    maxEdge / widthPt,
    maxEdge / heightPt,
    Math.sqrt(maxPixels / widthPt / heightPt),
  );
  if (!(scale > 0) || !Number.isFinite(scale)) {
    throw new RangeError('Dimensions cannot be represented at this budget');
  }
  const width = Math.max(1, Math.floor(widthPt * scale));
  const height = Math.max(1, Math.floor(heightPt * scale));
  if (width > maxEdge || height > maxEdge || width * height > maxPixels) {
    throw new RangeError('Raster cannot fit the budget on an integer pixel grid');
  }
  return { width, height, scale, effectiveDpi: scale * 72 };
}

4,096pxと8,000,000画素は、このサンプルで選んだアプリ側の方針です。すべての端末に安全な値ではありません。Canvasの最大サイズは環境によって異なります。制限内でも、確保・描画に失敗する経路は残ります。

この既定値をA4・300 DPIに適用すると、出力は2378 × 3363px、整数丸め前の倍率に対応するDPIは約287.67になります。UIでは「要求300 DPI」と「実際の出力寸法」を区別する必要があります。上限を適用した後も300 DPIだと表示すると、ユーザーが結果を誤解します。

極端に細いページにも注意が必要です。最小1pxを確保すると面積上限を超える場合があるので、最後に整数の幅・高さを再検査し、条件に収まらなければ拒否します。

描画せずに確認できることを先にテストする

次を同じディレクトリに check.mjs として置き、node check.mjs で実行します。検証用のLetterページは612 × 792ポイントです。A4と混同しないよう、寸法を明示します。

import assert from 'node:assert/strict';
import { fitRaster } from './raster-budget.mjs';

const roomy = { maxEdge: 10_000, maxPixels: 16_000_000 };
const low = fitRaster(612, 792, 150, roomy);
const high = fitRaster(612, 792, 300, roomy);
assert.deepEqual([low.width, low.height], [1275, 1650]);
assert.deepEqual([high.width, high.height], [2550, 3300]);
assert.equal(high.width * high.height / (low.width * low.height), 4);

const capped = fitRaster(612, 792, 300);
assert.ok(capped.width <= 4096 && capped.height <= 4096);
assert.ok(capped.width * capped.height <= 8_000_000);
assert.throws(() => fitRaster(612, 792, NaN), RangeError);
assert.throws(() => fitRaster(612, 792, 300, { maxPixels: 0 }), RangeError);
console.log('Pixel math and budget checks passed');

これは寸法、倍率、上限、異常値の拒否を確かめるテストです。Canvasを作らないため、実端末での描画成功やピークメモリは保証しません。そちらは実際のPDFと対象ブラウザで別に検証します。

複数ページでは「同時実行」と「保持済み出力」を別に見る

1ページの上限があっても、全ページを無制限の Promise.all で描画すれば、複数の描画とエンコードが重なります。逐次実行か、小さな上限を持つワーカープールにします。PDF.jsの例でも、同じCanvasで複数ページを同時描画できないと説明されています。

描画・エンコードを待ってから次に進み、不要な参照は解放します。一時Canvasの寸法を finally でリセットする場合も、widthへの代入で描画コンテキストがリセットされるという挙動と、OS上のメモリ表示がすぐ減るという保証は別です。

さらに、完成済みのJPEG BlobをZIP用に保持するなら、blob.size の合計には別の予算が必要です。これはエンコーダーやZIP作成中の一時領域までは数えません。途中で止まった場合は、完成ページ数と停止理由を表示し、全ページ成功と同じ通知にしないようにします。

実装を見直すときは、入力のDPI、確保する幅・高さ、同時ジョブ数、保持したファイル容量をそれぞれ確認すると、どの制限が効いているのか追いやすくなります。

元記事と参考資料

この記事は自著の英語版(DEV)を、日本語の実装メモとしてA4の例・UIの注意点・検証手順に組み直したものです。コードは共通ですが、本文の構成と具体例を変更しています。APIの説明は本文中のMDNとPDF.js公式資料を参照してください。

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?