印刷、アーカイブ、高解像度抽出のためにPDFを600 DPIでJPGに変換する必要がある場合、短い答えは:動作しますが、出力に制限をかけないとダメです。大きなPDFページを600 DPIで変換すると、1ページあたり数ギガバイトのJPGが生成されることがあります。メモリ予算のないサーバーではクラッシュしてしまいます。私はConvertifyの本番環境でこの問題に遭遇し、ユーザーのリクエストに可能な限り応えつつ、プロセスが暴走しないような修正を構築する必要がありました。
この記事では、Rust + libvipsで書いた制限関数、背後にある数学(ネタバレ:sqrt)、本番環境での実数値、そしてトレードオフについて解説します。ツールをすぐに使いたい方は、こちらから600 DPI PDF→JPG変換ツールをご利用ください。無料で登録不要で、以下で説明する制限がすべてのリクエストで実行されています。
600 DPIが重要な理由
「PDFを画像に」変換する際のDPIは、PDFの物理的なプロパティではありません。これはスケーリング係数です。libvipsにPDFページを600 DPIで読み込むよう指示すると、元の文書寸法の1インチあたり600ピクセルでラスタライズすることを意味します。したがって、出力サイズはPDFがどれだけの大きさだと認識しているかに完全に依存します。
ほとんどの場合、300 DPIで十分です。印刷、ほとんどの雑誌、高品質な写真出力では、300 DPIが標準です。600 DPIが必要になるのは以下のような場合です:
- アーカイブスキャン。 元の文書のあらゆる詳細を保持したい場合。
- 大判の高品質印刷:ポスター、技術図面、設計図。
- 高密度レイアウトのOCR。 小さいフォントや特殊なグリフは、高いラスタ解像度で恩恵を受けます。
- 大判文書。 地図、設計図、ワイドフォーマット図面。ここで最初に問題が発生しました。これらのPDFはしばしば巨大なネイティブ寸法(24インチ × 36インチ以上)を持ち、600 DPIでは爆発的に大きくなります。
誰かが検索エンジンにpdf to jpg 600 dpiと入力する場合、通常はこれらのニーズのいずれかを持っています。そして、サイレントに300 DPIにダウングレードされるツールを望んではいません。
素朴なアプローチ(そしてなぜ失敗したか)
libvipsでPDFページを指定DPIで読み込むのは1行です:
let img = VipsImage::new_from_file(&format!("{file_path}[dpi=600,page={page}]"))?;
フルスクリーンモードに入る フルスクリーンモードを終了
これだけです。libvips-popplerがレンダリングを処理します。典型的なA4ページでは、約3500万ピクセルの画像が得られ、JPGとして8〜15 MB程度です。問題ありません。
ここに大判PDF、例えば24インチ × 36インチの技術図面を貼り付けてみましょう。同じコードで3億ピクセル以上の画像が生成されます。メモリ内の生ピクセルデータとして、それは1 GBを超えます。品質90でJPGとしてエンコードしても、1ページあたり200〜400 MBになることがあります。600 DPIの複数ページPDF:ユーザーが何が起こっているかを知る前に、しばしばギガバイト規模の出力になります。
本番環境で見た実際の症状:
- 変換途中でサーバーのOOMキルが発生。
- ダウンロードが開始されたものの、ブラウザが処理できないほどファイルが大きすぎて完了しない。
- 「小さな」PDFが巨大な出力を生成したとユーザーが不満を述べる。
手抜きの修正は、DPIをグローバルに300に制限することです。しかし、それは正直なユースケースを罰することになります:通常サイズのPDFで正当に600 DPIを求める人は、奇妙なケースのためにサイレントダウングレードを強いられます。そこで私は適応型のソリューションが必要でした。
メガピクセル予算
実際の制約はDPIではありません。出力ピクセル数です。3500万ピクセルのJPGと3億5000万ピクセルのJPGは、それらを生成したDPIに関係なく、まったく異なるものです。
したがって、DPIを直接制限するのではなく、メガピクセル予算を設定しました:「出力画像はNメガピクセルを超えてはならない」。そして、すべての変換の前に低DPIでページ寸法をプローブし、リクエストされたDPIで生成されるであろうサイズを計算し、予算を超える場合は、収まるようにDPIを十分に下げます。
Convertifyでは、現在予算を100 MPに設定しています。これは通常の文書での600 DPIには十分すぎるほどです(標準的なレターサイズページを600 DPIで変換しても約34 MPにしかなりません)。これは病的なケースでのみ発動します:巨大なPDF、または入力がサーバーが正直に配信できる範囲を超えるエッジケースのジオメトリです。
制限関数
以下が本番環境で稼働している実際の関数です:
pub fn clamp_dpi_to_megapixels(
file_path: &str,
page: i32,
requested_dpi: i32,
max_megapixels: f64,
) -> i32 {
let probe = VipsImage::new_from_file(&format!("{file_path}[dpi=72,page={page}]"));
let (w, h) = match probe {
Ok(img) => (img.get_width() as f64, img.get_height() as f64),
Err(_) => return requested_dpi,
};
let scale = requested_dpi as f64 / 72.0;
let mp = (w * scale) * (h * scale) / 1_000_000.0;
if mp <= max_megapixels {
requested_dpi
} else {
let factor = (max_megapixels / mp).sqrt();
((requested_dpi as f64 * factor).floor() as i32).max(72)
}
}
フルスクリーンモードに入る フルスクリーンモードを終了
順を追って説明します。
72 DPIでプローブ。 libvips-popplerは72 DPIをPDFのネイティブな「ポイント」グリッドとして扱うため、72で読み込むのは安価で、ポイント単位での真のページ寸法が得られます。8.5インチ × 11インチのページは612 × 792ピクセルとして返されます。
プローブが失敗した場合(ページ番号が範囲外、PDFの破損、その他)、リクエストされたDPIを変更せずに返します。下流の変換は適切なエラーで失敗します。ここでそれを飲み込むべきではありません。
リクエストされたDPIで生成されるサイズを計算。 基本寸法にrequested_dpi / 72を掛けてから、メガピクセルに変換します。同じ8.5インチ × 11インチのページを600 DPIで変換した場合:(612 × 8.33) × (792 × 8.33) / 1_000_000 ≈ 33.6 MP。予算内です。
予算内なら変更せず返す。 素朴なDPIで最大MP以下の出力が得られる場合、完了です。これがハッピーパスで、おそらく95%以上のユーザー要求に該当します。
予算超過なら比例して下げる。 ここで数学が重要になります。350 MPを100 MPに下げるには、100/350の係数で縮小する必要があります。しかし、この比率はピクセルのものです。DPIはピクセルを2次関数的に縮小します。DPIを半分にすると、ピクセルは4分の1になります。したがって、DPIの係数はsqrt(100/350) ≈ 0.53です。600 DPIに適用すると:600 × 0.53 ≈ 318 DPI。安全なフォールバックである300より十分に高いですが、収まる程度に低くなっています。
.floor()と.max(72)は安全装置です。Floorは予算を超えて切り上げるのを防ぎ、72は実効最小値です。PDFのネイティブグリッドを下回る解像度でラスタライズするとゴミが出力されるためです。
なぜsqrtか(数学の小話)
ここに特別なものはありませんが、これは全体の洞察なので明確に述べておく価値があります。
ピクセル数はDPIの2乗に比例してスケールし、DPI自体に比例するわけではありません。DPIを2倍にすると、幅と高さの両方が2倍になるため、ピクセル数は4倍になります。DPIを3倍にすると、ピクセル数は9倍になります。関係式は:
pixels = base_pixels × (dpi / 72)²
フルスクリーンモードに入る フルスクリーンモードを終了
ターゲットピクセル数がわかっている場合のDPIの解:
dpi = 72 × sqrt(target_pixels / base_pixels)
フルスクリーンモードに入る フルスクリーンモードを終了
これがまさに制限関数が行っていることで、requested_dpiを通じて因数分解されています。sqrtをスキップしてピクセル比率でDPIを直接掛けると、過剰補正になります。予算が実際に許容するよりもはるかに低いDPIになってしまい、ユーザーは必要以上に低解像度の出力を受け取ることになります。
オンラインのいくつかの場所で「比例スケーリングによるDPI制限」が書かれているのを見たことがあります。それらの出力は、本来あるべきサイズの何分の一かになってしまいます。これがその理由です。
本番環境での実数値
以下が、100 MP予算での制限関数の実際の動作です:
| 入力文書 | 72 DPIでのベース | リクエスト | リクエスト時のMP | 制限後のDPI | 出力MP |
|---|---|---|---|---|---|
| レター (8.5インチ × 11インチ) | 612 × 792 (0.48 MP) | 600 | 33.6 MP | 600 | 33.6 |
| A3 (11.7インチ × 16.5インチ) | 842 × 1191 (1.0 MP) | 600 | 69.6 MP | 600 | 69.6 |
| 大判設計図 (24インチ × 36インチ) | 1728 × 2592 (4.5 MP) | 600 | 311 MP | 340 | 100 |
| ワイド図面 (36インチ × 60インチ) | 2592 × 4320 (11.2 MP) | 600 | 778 MP | 215 | 100 |
パターン:通常の文書では、600 DPIは変更されずに通過します。素朴なアプローチで数百メガピクセルを生成するようなエッジケースでは、DPIは予算に収まるようにスケールバックされ、ユーザーはグローバルな300 DPIフォールバックよりも意図に近い出力を受け取ります。
TIFFを使うべき場合
注意すべき点:600 DPIでアーカイブ作業を行う場合、JPGはしばしば不適切です。JPGは非可逆圧縮であり、高DPIでは特に細部を保持したい場合、通常はロスレスを望むからです:テキストの端、細い線、小さな注釈。
そのような場合、TIFFが適切な形式です。LZW圧縮TIFFは、合理的なファイルサイズでロスレスであり、地球上のすべてのアーカイブシステムが読み取れます。libvipsはネイティブにTIFFを扱います。
私はPDF→TIFF変換ツールの背後でも同じ制限を提供しています。エンコーダーだけが異なります。似たようなものを構築する場合、600 DPIのアーカイブ作業にJPGを強制しないでください。TIFFもサポートしてください。ユーザーは感謝するでしょう。
もしやり直すなら変えること
2つのことです。
ページごとのフィードバック。 現在、制限はサイレントで実行されます。200ページの文書で、1つの地図ページが600から340 DPIにスケールされた場合:ユーザーはそれを見ません。ほとんどのケースではサイレントが親切ですが、アーカイブワークフローでは、どのページが予算にヒットしたかを正確に知りたいものです。レスポンスでそれを表面化するでしょう。
適応型予算。 100 MPはハードコードされた定数です。より大きなサーバーでは250にできるかもしれません。モバイルのみのセッションでは、40の方が理にかなっています。リクエストコンテキストから予算を導出する(またはユーザーがより高いティアを選択できるようにする)ことは、小さな設定変更で実際の影響を与えるでしょう。
どちらも緊急ではありませんが、両方ともリストにあります。
試してみる
今すぐ600 DPI PDF→JPG変換が必要な場合:Convertifyがそれを行います。無料、登録不要、最大10ファイルのバッチ処理が可能で、上記の制限関数がすべてのリクエストで実行されます。巨大なファイルをアップロードすると、妥当なメモリ予算に収まる最高のDPIが得られます。クラッシュではなく、サイレントなグローバルダウングレードでもありません。
独自のPDF→画像ツールを構築していて同様の問題に遭遇した場合、上記の関数は本番環境でテスト済みで、そのままドロップインできるほど短いです。max_megapixelsをインフラストラクチャに合わせて調整して進めてください。
Convertifyのエンジニアリングについてもっと知りたい場合は、このアカウントでBuilding Convertify in Publicに毎週書いています。libvipsのトリック、Rustのパターン、本物の本番バグ。無駄な内容はありません。
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.