{"record":{"id":"b32f629cb0a94ed6","repo":"we-promise/sure","slug":"could-not-convert-pdf-to-images","errorCode":null,"errorMessage":"Could not convert PDF to images","messagePattern":"Could not convert PDF to images","errorType":"exception","errorClass":"Provider::Openai::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/openai/pdf_processor.rb","lineNumber":154,"sourceCode":"      text_parts = []\n\n      reader.pages.each_with_index do |page, index|\n        text_parts << \"--- Page #{index + 1} ---\"\n        text_parts << page.text\n      end\n\n      text_parts.join(\"\\n\\n\")\n    rescue => e\n      Rails.logger.error(\"Failed to extract text from PDF: #{e.message}\")\n      nil\n    end\n\n    def process_with_vision\n      effective_model = model.presence || Provider::Openai::DEFAULT_MODEL\n\n      # Convert PDF to images using pdftoppm\n      images_base64 = convert_pdf_to_images\n      raise Provider::Openai::Error, \"Could not convert PDF to images\" if images_base64.blank?\n\n      # Build message content with images (max 5 pages to avoid token limits)\n      content = []\n      images_base64.first(5).each do |img_base64|\n        content << {\n          type: \"image_url\",\n          image_url: {\n            url: \"data:image/png;base64,#{img_base64}\",\n            detail: \"low\"\n          }\n        }\n      end\n      content << {\n        type: \"text\",\n        text: \"Please analyze this PDF document (#{images_base64.size} pages total, showing first #{[ images_base64.size, 5 ].min}) and respond with valid JSON only.\"\n      }\n\n      # Note: response_format is not compatible with vision, so we ask for JSON in the prompt","sourceCodeStart":136,"sourceCodeEnd":172,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/openai/pdf_processor.rb#L136-L172","documentation":"Raised by Provider::Openai::PdfProcessor#process_with_vision when convert_pdf_to_images returns an empty array. That helper writes the bytes to a temp file, shells out via system(\"pdftoppm\", \"-png\", \"-r\", \"150\", ...) — poppler-utils — and globs page-*.png; every exception is rescued to [] and system failures (nonzero exit, command not found) simply leave no files. So the two root causes are pdftoppm missing from PATH (typical slim Docker images) and pdftoppm failing on the PDF itself (encrypted, corrupt, or unsupported).","triggerScenarios":"Deploying to alpine/slim Docker without installing poppler-utils (system returns nil, zero PNGs); pdftoppm erroring 'Incorrect password' on an encrypted PDF; malformed PDF making pdftoppm exit nonzero; pdf_content blank hitting the early return [].","commonSituations":"Works on the dev Mac (poppler preinstalled via brew), fails in production container; CI pipeline lacking system packages; encrypted statements reaching the vision fallback; disk-full temp dirs making writes fail silently inside the rescue.","solutions":["Verify pdftoppm exists in the runtime image: docker run --rm <image> which pdftoppm; add poppler-utils (Debian/Ubuntu: apt-get install -y poppler-utils; Alpine: apk add poppler-utils).","Capture pdftoppm's exit status and stderr in convert_pdf_to_images instead of discarding them, so missing-binary vs bad-PDF is distinguishable.","Fail fast at boot/health check: raise a clear configuration error if system('which', 'pdftoppm') fails.","For encrypted PDFs, decrypt before the vision path."],"exampleFix":"# before\nsystem(\"pdftoppm\", \"-png\", \"-r\", \"150\", pdf_path, output_prefix)\nimage_files = Dir.glob(File.join(tmpdir, \"page-*.png\")).sort\n\n# after\nok = system(\"pdftoppm\", \"-png\", \"-r\", \"150\", pdf_path, output_prefix, err: \"/tmp/pdftoppm.err\")\nRails.logger.error(\"pdftoppm failed (#{$?.exitstatus}): #{File.read('/tmp/pdftoppm.err')}\") unless ok\nimage_files = Dir.glob(File.join(tmpdir, \"page-*.png\")).sort","handlingStrategy":"validation","validationCode":"raise RuntimeError, \"pdftoppm (poppler-utils) is not installed\" unless system(\"which\", \"pdftoppm\", out: File::NULL, err: File::NULL)","typeGuard":null,"tryCatchPattern":"begin\n  processor.process\nrescue Provider::Openai::Error => e\n  raise unless e.message.include?(\"Could not convert PDF to images\")\n  notify_ops(\"vision PDF path broken: check poppler-utils in the runtime image\")\nend","preventionTips":["Install poppler-utils in every runtime image (dev, CI, production) and assert its presence at boot.","Capture pdftoppm exit status and stderr instead of discarding them, so missing binary vs bad PDF is obvious.","Add a smoke test that converts a one-page fixture PDF through the vision path in CI."],"tags":["openai","pdf","pdftoppm","poppler","system-dependency","docker"],"backgroundTag":"missing-system-dependency","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}