Class: Pdf::Utility::CliRasterizer
- Inherits:
-
Object
- Object
- Pdf::Utility::CliRasterizer
- Defined in:
- app/services/pdf/utility/cli_rasterizer.rb
Overview
Service object: rasterize a single PDF page to PNG via the pdftoppm (poppler)
CLI in a short-lived subprocess.
Why not libvips: Rails 8.1.3.1 (CVE-2026-66066, GHSA-xr9x-r78c-5hrm) calls
+Vips.block_untrusted(true)+ process-wide, and pdfload is an untrusted loader,
so every in-process vips PDF load fails with "not a known file format".
Unblocking around user-supplied PDFs is the exact threat class the CVE patch
targets, so every PDF raster path isolates the parse in a subprocess instead
— poppler's attack surface is contained to a short-lived, rlimit-capped child
(AppSignal #1983, #6487, #6491, and #6492).
The caller owns the returned Tempfile (same convention as PageRasterizer).
Defined Under Namespace
Classes: RasterizationError
Constant Summary collapse
- MAX_OUTPUT_PIXELS =
A 32-megapixel RGB page is roughly 96 MiB once decoded. This accommodates
11×17-inch pages at 400 DPI while rejecting poster-sized or forged PNG
dimensions before libvips sees them. 32_000_000- MAX_OUTPUT_BYTES =
Bound the on-disk artifact independently of its claimed dimensions.
128 * 1024 * 1024
- PNG_SIGNATURE =
Poppler is required to emit PNG; verify that before reading its IHDR.
"\x89PNG\r\n\x1A\n".b
Class Method Summary collapse
-
.page_count(pdf_path) ⇒ Integer
Count pages through pdfinfo in the same bounded subprocess used for rasterization.
-
.render_page(pdf_path, page: 0, dpi: 150) ⇒ Tempfile
Render +page+ (zero-based) of +pdf_path+ to a PNG Tempfile.
Class Method Details
.page_count(pdf_path) ⇒ Integer
Count pages through pdfinfo in the same bounded subprocess used for
rasterization. This keeps PDF parsing outside the Rails process.
38 39 40 41 42 43 44 45 46 |
# File 'app/services/pdf/utility/cli_rasterizer.rb', line 38 def page_count(pdf_path) validate_path!(pdf_path) stdout, stderr, status = run_command(['pdfinfo', pdf_path]) pages = stdout[/^Pages:\s+(\d+)$/i, 1].to_i return pages if status.success? && pages.positive? raise RasterizationError, "pdfinfo failed (exit #{status.exitstatus || 'unknown'}): #{stderr.presence || stdout}".strip end |
.render_page(pdf_path, page: 0, dpi: 150) ⇒ Tempfile
Render +page+ (zero-based) of +pdf_path+ to a PNG Tempfile.
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
# File 'app/services/pdf/utility/cli_rasterizer.rb', line 56 def render_page(pdf_path, page: 0, dpi: 150) validate_path!(pdf_path) Dir.mktmpdir('pdf_raster') do |dir| output_prefix = File.join(dir, 'page') # -singlefile writes exactly one file (<prefix>.png, no -N suffix); # pdftoppm pages are 1-based, hence page + 1. cli_page = (page.to_i + 1).to_s cmd = ['pdftoppm', '-png', '-r', dpi.to_i.to_s, '-f', cli_page, '-l', cli_page, '-singlefile', pdf_path, output_prefix] stdout, stderr, status = run_command(cmd) output = "#{output_prefix}.png" raise RasterizationError, "pdftoppm failed (exit #{status.exitstatus || 'unknown'}): #{stderr.presence || stdout}".strip unless status.success? && File.exist?(output) validate_output!(output) png = Tempfile.new(['pdf_page_', '.png']) png.binmode File.open(output, 'rb') { |source| IO.copy_stream(source, png) } png.flush png.rewind png end end |