Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

Direct answer: load the records your document needs, combine them into a prepared data object, render PDF bytes with a generator such as Prawn (or render an HTML view with Wicked PDF), and return the result with Rails send_data. Use send_file only when a PDF already exists on disk. Keeping data loading separate from document layout makes a report assembled from several Active Record models easier to test and maintain.

Choose the PDF generation approach first

Your layout and deployment environment determine the right implementation. Prawn writes the document through Ruby drawing and text APIs. Wicked PDF renders an HTML view through the external wkhtmltopdf executable. Neither is universally best; check your Rails version, host, styling requirements and expected workload before locking dependencies.

Requirement Approach Important trade-off
Layout can be expressed with PDF text and drawing primitives Prawn Direct Ruby PDF authoring; it is not an HTML/CSS view. Review the versioned Prawn documentation and lock the tested gem version.
An existing HTML template and CSS should drive the document Wicked PDF Familiar view-based authoring, but you must install and support wkhtmltopdf and configure assets for a process running outside Rails.
Generated bytes are held in memory send_data Streams the generated response directly to the client.
A PDF already exists on disk send_file Streams a file path; use a controlled, application-owned path.

Rails documents that controllers provide both methods for streaming responses in its Action Controller Advanced Topics guide. The Rails 6.1 guide also demonstrates generating a Prawn PDF and passing the resulting string to send_data; verify examples against the Rails version your application actually runs.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Model the report as one prepared input

Do not make the PDF class query five unrelated models while it is drawing pages. Fetch the root record and associations in the controller or an application service, then pass a hash or value object containing everything the document needs. This avoids hidden database calls during rendering and gives you one place to enforce authorization and consistency.

Example data loader

class ReportData
  def self.load(id)
    report = Report.includes(:customer, :line_items, :payments).find(id)

    {
      report: report,
      customer: report.customer,
      line_items: report.line_items,
      payments: report.payments,
      generated_at: Time.current
    }
  end
end

Use includes (or an equivalent preload strategy) for associations used in the document to avoid an N+1 query pattern. Authorize the report before loading related data, and decide whether all sections must represent one transactionally consistent snapshot. For large reports, consider a background job and a stored result rather than holding a long request open.

Generate a PDF with Prawn and send_data

Prawn is suitable when the report can be laid out with Ruby APIs. The following controller keeps retrieval, generation and HTTP response responsibilities distinct. ReportPdf is application code, not a Rails class.

class ReportsController < ApplicationController
  def show
    report_data = ReportData.load(params[:id])
    pdf_bytes = ReportPdf.new(report_data).render

    send_data pdf_bytes,
      filename: "report-#{params[:id]}.pdf",
      type: "application/pdf",
      disposition: "attachment"
  end
end

class ReportPdf
  def initialize(data)
    @report = data.fetch(:report)
    @customer = data.fetch(:customer)
    @line_items = data.fetch(:line_items)
    @payments = data.fetch(:payments)
    @generated_at = data.fetch(:generated_at)
  end

  def render
    Prawn::Document.new do |pdf|
      pdf.text "Report #{@report.id}", size: 22, style: :bold
      pdf.move_down 8
      pdf.text "Customer: #{@customer.name}"
      pdf.text "Generated: #{@generated_at.iso8601}"
      pdf.move_down 16

      pdf.text "Line items", size: 16, style: :bold
      rows = [["Description", "Quantity", "Amount"]] + @line_items.map do |item|
        [item.description.to_s, item.quantity.to_s, format("%.2f", item.amount)]
      end
      pdf.table(rows, header: true, width: pdf.bounds.width)

      pdf.move_down 16
      pdf.text "Payments", size: 16, style: :bold
      @payments.each do |payment|
        pdf.text "#{payment.paid_at}: #{format('%.2f', payment.amount)}"
      end
    end.render
  end
end

Install Prawn in the application and consult its versioned manual for tables, page breaks, fonts, images and repeatable headers. Escape or normalize user-provided text as appropriate for the API you use. Add tests that assert the response content type, filename and that the returned bytes begin with a valid PDF signature; integration tests should also exercise page overflow and empty associations.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Controller details that matter

  • MIME type: use application/pdf (the type: option shown above).
  • Download versus inline display: disposition: "attachment" prompts a download; use inline when browser preview is desired.
  • Filenames: derive names from trusted identifiers and sanitize user-controlled text.
  • Authorization: perform the same policy checks as the HTML report endpoint; generating a PDF must not bypass them.
  • Memory: send_data holds generated bytes until the response is sent. Large documents may need asynchronous generation and object storage.

Use Wicked PDF when HTML is the natural source

Wicked PDF lets you render a Rails view as a PDF, but it wraps the external wkhtmltopdf executable. Install the gem and a compatible executable for your deployment, then verify the exact compatibility guidance in the project README.

class ReportsController < ApplicationController
  def show
    @report_data = ReportData.load(params[:id])

    render pdf: "report-#{params[:id]}",
      template: "reports/show",
      formats: [:html]
  end
end

Create app/views/reports/show.html.erb with the combined customer, line-item and payment data. The PDF process runs outside Rails, so relative asset paths that work in a normal browser may fail. Configure Wicked PDF’s asset handling, use absolute references where required, and test fonts, images, stylesheets and JavaScript in the same container or host that serves production PDFs. A missing executable, inaccessible asset or unsupported CSS rule commonly produces a blank or partially styled document.

When to avoid this path

  • Use Prawn when you cannot install or maintain an external renderer.
  • Use Wicked PDF when reproducing an existing HTML layout is more valuable than direct PDF control.
  • Do not assume browser-perfect CSS support; renderer behavior depends on the installed executable and its options.

Keep multi-model data correct and efficient

Prevent inconsistent reads

Load all required records under the authorization context. If a report combines mutable invoices, payments and customer details, decide whether a transaction or a persisted report snapshot is needed. Otherwise, one model may reflect a later state than another by the time rendering finishes.

Control query count and ordering

Preload associations, select only columns needed for very large datasets, and apply an explicit ordering before passing collections to the generator. Never rely on database default order for a customer-facing document.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Handle empty and large collections

Render an explicit “No payments recorded” or equivalent message for empty associations. For thousands of rows, paginate or summarize, insert page breaks deliberately, and move work to a background job. A request timeout is not fixed by changing the response MIME type.

Make output deterministic

Pass a fixed report timestamp, locale, currency and timezone into the generator. Pin gem and executable versions in deployment, and compare generated PDFs in integration tests after dependency upgrades.

Common failures and fixes

Symptom Likely cause Fix
Browser downloads HTML or shows a Rails error page An exception occurred before PDF bytes were returned, or the response was rendered as HTML. Inspect server logs, ensure the generator returns bytes, and set type: "application/pdf".
uninitialized constant Prawn The gem is absent or not loaded. Add the dependency, run bundle installation in the deployment environment, and require it according to the selected version’s instructions.
Wicked PDF cannot find wkhtmltopdf The executable is not installed or is outside the process PATH. Install a compatible binary, configure its path, and test it inside the production image.
Styles or images disappear The external renderer cannot resolve Rails-relative assets. Configure Wicked PDF asset helpers or absolute URLs, and verify permissions and network access from the renderer process.
Report is slow or times out N+1 queries, huge collections, expensive images or synchronous rendering. Preload associations, reduce data, optimize assets and generate large reports asynchronously.
Rows appear in a different order on each run No explicit ordering was applied. Order the relation before placing it in the prepared data object.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Response and deployment checklist

  1. Authorize the requested root record.
  2. Load all participating models with an intentional preload and ordering strategy.
  3. Build a single report-data object and pass it to Prawn or an HTML view.
  4. Pin and test the Prawn gem or Wicked PDF plus wkhtmltopdf versions used in production.
  5. Verify fonts, images, page breaks, empty states, locale, timezone and currency.
  6. Return generated bytes with send_data, or use send_file for a trusted existing path.
  7. For large jobs, persist the result and expose a controlled download endpoint.

Or skip the browser setup

If your goal is to capture a public report page as an image or PDF rather than generate a semantic Rails PDF, ScreenshotNeo provides a one-request API and an MCP server for AI agents. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups and chat widgets; those cleanup steps can be disabled individually. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads and cache hits are not billed, and response headers identify the page verdict and billing status.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com/reports/123 -o report.webp

See the ScreenshotNeo API documentation for PDF capture, authentication and options such as full-page lazy-image loading, CSS-selector element capture, device and retina settings, custom CSS or JavaScript, click and wait conditions, request blocking, cookies, headers, timezone, geolocation, resizing, caching, signed links, asynchronous webhooks, bulk capture and usage reporting. Its MCP tools are take_screenshot, get_page_info and capture_pdf.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots; every feature is available on every plan. Sign up for ScreenshotNeo free.

Frequently Asked Questions

Should PDF generation live in the controller?

A small report can use a private controller method, but a dedicated generator or service is easier to test when several models and layout rules are involved.

Can I use both Prawn and Wicked PDF in one Rails application?

Yes. Choose per document, but keep dependency versions, executable availability and rendering tests explicit for each path.

Does send_data save the PDF automatically?

No. It sends generated bytes in the HTTP response. Persist the bytes separately if users need a later download without regenerating the document.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Bottom Line

Prepare one authorized, consistent data object from all required models, render it with Prawn or Wicked PDF according to your layout and deployment constraints, and return the result with send_data. Validate the complete stack in the same environment that will generate production documents.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.