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.

In Grails, the Rendering Plugin converts a GSP that outputs well-formed XHTML into a PDF. Use pdfRenderingService.render(...) when your application needs PDF bytes or an output stream; use the controller’s renderPdf(...) method when the browser should download the generated document.

The plugin reference reviewed for this guide is version 1.0.0 and does not publish a compatibility matrix for current Grails releases. Confirm the dependency coordinates and supported Grails version for your application before deploying.

Choose the output path

Both documented APIs render a GSP template through the XHTML Renderer library. The choice is determined by where the result must go.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
Need Use What you receive
Store, email, post-process, or test the PDF in application code pdfRenderingService.render Generated bytes by default, or data written to an OutputStream
Return a download from a Grails controller renderPdf An HTTP PDF response, with filename and content type options

The service API accepts a map containing a required template and optional model, plugin, and controller. A template name maps to a GSP partial, so report normally means a file named _report.gsp.

Check version and application prerequisites

Verify the plugin pairing

The reference documentation identifies the Grails Rendering Plugin as version 1.0.0. The Apache Grails documentation landing page lists framework documentation for Grails 7.2.4, 7.1.7, and 7.0.17, but the reviewed pages do not state that plugin 1.0.0 supports any of those releases. Check your build’s dependency metadata, the plugin release information, and your Grails version together; do not assume that a framework upgrade preserves rendering compatibility.

Reference links: Grails Rendering Plugin reference documentation and Apache Grails documentation.

Prepare the template inputs

  • Put the report view under the application’s views directory, for example grails-app/views/pdfs/_report.gsp.
  • Make the GSP produce valid, well-formed XHTML, not merely forgiving browser HTML.
  • Declare an XHTML doctype. Without one, entities such as   can fail to resolve.
  • Ensure every stylesheet and image can be fetched by the server-side renderer.

Create a PDF GSP that the renderer can parse

A minimal report template can look like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
  <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
  <title>${report.title}</title>
  <style type="text/css">
    @page { size: 210mm 297mm; margin: 18mm; }
    body { font-family: sans-serif; font-size: 11pt; color: #222; }
    h1 { font-size: 20pt; }
    table { width: 100%; border-collapse: collapse; }
    th, td { border: 0.2mm solid #999; padding: 2mm; }
  </style>
</head>
<body>
  <h1>${report.title}</h1>
  <p>Prepared for ${report.customerName}</p>
  <table>
    <tr><th>Item</th><th>Amount</th></tr>
    <g:each in="${report.items}" var="item">
      <tr>
        <td>${item.description}</td>
        <td>${item.amount}</td>
      </tr>
    </g:each>
  </table>
</body>
</html>

The @page rule illustrates the documented way to set an A4-sized page (210 mm by 297 mm). Add print-specific CSS deliberately; browser-only features and malformed markup may not be understood by the XHTML renderer.

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.

Render PDF bytes in a service or other application code

Call the service when another part of the application needs the generated document:

class ReportPdfService {
    def pdfRenderingService

    byte[] createReportPdf(report) {
        pdfRenderingService.render(
            template: '/pdfs/report',
            model: [report: report]
        )
    }
}

With no second argument, render returns output from a new ByteArrayOutputStream. To write directly to a destination you control, provide an output stream:

def writeReportPdf(report, OutputStream destination) {
    pdfRenderingService.render(
        template: '/pdfs/report',
        model: [report: report]
        , destination
    )
}

In normal Groovy style, pass the stream as the method’s second argument rather than as another map entry:

pdfRenderingService.render(
    [template: '/pdfs/report', model: [report: report]],
    destination
)

A relative template path is resolved from the controller’s views directory and therefore needs controller context. A path beginning with /, such as /pdfs/report, resolves from the application’s views directory.

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.

Return a downloadable PDF from a controller

For a browser download, let the controller supply request context and response headers:

class ReportController {
    def download() {
        def report = reportService.find(params.id)
        if (!report) {
            render status: 404, text: 'Report not found'
            return
        }

        renderPdf(
            template: '/pdfs/report',
            model: [report: report],
            filename: report.name
        )
    }
}

filename sets the Content-Disposition attachment name. The documented default content type is application/pdf; specify contentType when your response policy requires it:

renderPdf(
    template: '/pdfs/report',
    model: [report: report],
    filename: 'invoice.pdf',
    contentType: 'application/pdf'
)

Use a filename ending in .pdf and sanitize any user-controlled name before passing it to a response header.

Make images, CSS, and fonts reachable

Images and stylesheets

The renderer, not the end user’s browser, resolves linked resources. The application must be able to serve every image and stylesheet referenced by the GSP. Relative URLs are resolved against grails.serverURL, so configure that value for the environment in which PDFs are generated. A path that works in a browser but points to a private, incorrect, or unreachable host from the server will produce a missing asset in the PDF.

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

When an image is available only as bytes, the plugin documents the rendering:inlinePng, inlineGif, and inlineJpeg tags. These generate data-URI-backed image tags and avoid a separate resource request.

Characters and embedded fonts

If glyphs do not render through the underlying iText setup, define an embedded font and its encoding in CSS with @font-face, using the renderer-specific -fs-pdf-font-embed and -fs-pdf-font-encoding properties. Test the actual character set used by names, currencies, and customer data rather than relying on a Latin-only sample.

Performance, buffering, and caching

PDF generation can be expensive because the template is parsed and laid out server-side. The reference describes two cacheable stages: the intermediate DOM Document and the final output bytes. Cache only when the inputs, permissions, locale, and data freshness make reuse safe; a cached PDF can otherwise expose stale or unauthorized information.

When writing a response, output is first buffered so the framework can calculate Content-Length. Supplying a destination stream can avoid that extra copy, but if you bypass buffering you must set Content-Length yourself when your HTTP infrastructure requires it. Measure memory usage for large reports and avoid constructing multiple byte arrays unnecessarily.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
  • Keep report queries bounded and paginate or summarize large datasets before rendering.
  • Reuse a parsed DOM only when the template and all variable content are handled safely.
  • Use deterministic asset URLs and immutable versioned CSS when caching rendered documents.
  • Exercise concurrent generation in a staging environment; renderer and font resources are shared server costs even when the request code is small.

Troubleshooting common failures

XmlParseException or malformed-document errors

Cause: the GSP emits invalid XHTML, an unclosed element, or an entity the parser does not know. Fix: add the XHTML doctype, close every element, quote attributes, escape ampersands, and replace unsupported entities such as &nbsp; with valid numeric or supported references.

The template cannot be found

Cause: the path does not match the underscore-prefixed GSP filename or is relative without controller context. Fix: store _report.gsp under the expected views directory and call it as /pdfs/report, or provide the controller context required by a relative path.

Images or CSS are missing

Cause: the renderer cannot reach the URL, or a relative URL resolves against the wrong grails.serverURL. Fix: use an application-reachable absolute URL, correct the server URL for the running environment, check authentication requirements, and verify the resource response from the Grails host itself.

Text appears as boxes or the wrong symbols

Cause: the selected font lacks the glyphs or was not embedded and encoded for PDF output. Fix: configure @font-face with the documented -fs-pdf-font-embed and -fs-pdf-font-encoding properties, then regenerate with representative text.

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

The download has the wrong name or opens as text

Cause: the controller did not set a filename or content type. Fix: pass filename: 'report.pdf' to renderPdf and use contentType: 'application/pdf' when overriding defaults.

Generation is slow or memory-heavy

Cause: large DOMs, high-resolution images, repeated rendering, or unnecessary byte copies. Fix: reduce input size, cache safe intermediate or final results, stream to a destination where appropriate, and profile concurrent requests before increasing worker counts.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

When a browser-based capture is the better fit

This plugin is a server-side GSP-to-PDF renderer. If the requirement is instead a snapshot of an already deployed page—including modern browser layout, JavaScript, or a public URL—use a browser capture service rather than rewriting that page as XHTML.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server. It accepts consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be disabled. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and each response reports the page verdict and billing status in X-Page-Verdict and X-Billed headers. It can return PNG, JPEG, WebP, or PDF and supports full-page capture, CSS-selector elements, device and viewport settings, JavaScript, custom headers and cookies, waiting rules, resource blocking, PDF page controls, signed links, asynchronous webhooks, bulk capture, and an MCP server for Claude, Cursor, or another MCP client.

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

One GET request is enough:

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

See the ScreenshotNeo API documentation for the URL and output options. The same request in Python is:

import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
    timeout=90,
)
r.raise_for_status()
open("shot.webp", "wb").write(r.content)

Node.js:

const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const data = Buffer.from(await res.arrayBuffer());

The Free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 shots, and every feature is included on every plan. Create a free ScreenshotNeo account.

FAQ

Does this plugin render arbitrary modern HTML from a URL?

No. Its documented input is a Grails GSP that produces well-formed XHTML. A deployed-page capture service is a different workflow.

Which API should a batch job use?

Use pdfRenderingService.render with an output stream or stored bytes when the job owns delivery or storage. Use renderPdf only when a controller request should produce the HTTP response.

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

Where should compatibility be confirmed?

Check the plugin’s versioned reference, your build’s resolved dependency metadata, and the Apache Grails release documentation together; the reviewed plugin guide does not provide a matrix for the listed Grails releases.

Frequently Asked Questions

Does this plugin render arbitrary modern HTML from a URL?

No. Its documented input is a Grails GSP that produces well-formed XHTML. A deployed-page capture service is a different workflow.

Which API should a batch job use?

Use pdfRenderingService.render with an output stream or stored bytes when the job owns delivery or storage. Use renderPdf only when a controller request should produce the HTTP response.

Where should compatibility be confirmed?

Check the plugin’s versioned reference, your build’s resolved dependency metadata, and the Apache Grails release documentation together; the reviewed plugin guide does not provide a matrix for the listed Grails releases.

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

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.