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.

To convert HTML to PDF bytes in Kotlin, pass the HTML to an HTML-to-PDF renderer that writes into a ByteArrayOutputStream, then call toByteArray() after conversion finishes. On Android, HTML printing is different: load the markup in a WebView and use its print adapter, which hands a print job to Android’s print framework rather than returning a synchronous ByteArray. Android’s PdfDocument writes bytes too, but it draws native pages; it does not lay out an HTML string.

The correct implementation therefore depends first on whether your Kotlin code runs in an Android app or on the JVM (for example, a server or desktop service).

Choose the conversion path before writing code

Runtime and input Appropriate path What you receive Important limitation
Android app, HTML/CSS WebView plus createPrintDocumentAdapter() An Android print job handled by print services The documented workflow is not a synchronous HTML-to-ByteArray API
Android app, native drawing android.graphics.pdf.PdfDocument Bytes written to an output stream You draw every page yourself; HTML is not parsed
JVM Kotlin, HTML/CSS iText pdfHTML or OpenHTMLtoPDF Direct PDF bytes from an output stream Neither should be assumed to have full browser fidelity

Android documents the WebView print route and the separate native PdfDocument API in its printing and API-reference material: Printing HTML documents and PdfDocument. For JVM conversion, consult the selected library’s version-specific API before fixing imports or dependency coordinates.

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

JVM Kotlin: return HTML as a PDF byte array

The general pattern is independent of framework: create a memory-backed output stream, configure the renderer (including a base URI when the HTML references files), convert, and only then read the bytes. Reading before conversion closes leaves an incomplete or empty result.

iText pdfHTML pattern

iText’s pdfHTML APIs accept an HTML string or input stream and write through a PdfWriter/PdfDocument or an output stream. The following Kotlin is an illustrative integration pattern based on those Java APIs; check the exact overload and package names for the pdfHTML release you select.

import com.itextpdf.html2pdf.ConverterProperties
import com.itextpdf.html2pdf.HtmlConverter
import java.io.ByteArrayOutputStream

fun htmlToPdfBytes(html: String, baseUri: String? = null): ByteArray {
    require(html.isNotBlank()) { "HTML must not be blank" }

    val output = ByteArrayOutputStream()
    val properties = ConverterProperties()
    if (baseUri != null) {
        properties.setBaseUri(baseUri)
    }

    HtmlConverter.convertToPdf(html, output, properties)
    return output.toByteArray()
}

fun main() {
    val html = """
        <!doctype html>
        <html><head>
          <meta charset='utf-8'>
          <style>body { font-family: sans-serif; }</style>
        </head><body>
          <h1>Invoice</h1><p>Generated from Kotlin</p>
        </body></html>
    """.trimIndent()

    val pdf: ByteArray = htmlToPdfBytes(html)
    java.nio.file.Files.write(java.nio.file.Path.of("invoice.pdf"), pdf)
}

The baseUri is essential when markup contains relative references such as <img src="images/logo.png"> or stylesheets. Use a URI that the renderer can read, and make sure the process has permission to access it. Absolute URLs may require network access and may make builds nondeterministic.

Returning bytes from a web endpoint

Keep conversion in a service function and set the response’s PDF content type at the HTTP layer. Do not convert a binary result to a text string.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
fun renderInvoice(): ByteArray {
    val html = loadInvoiceTemplate()       // your template code
    return htmlToPdfBytes(html, "file:///opt/app/templates/")
}

// Framework-specific controller code should return:
// Content-Type: application/pdf
// Body: renderInvoice()

For large documents, remember that the output stream and the returned array coexist briefly, so peak heap use is higher than the final PDF size. If your web framework accepts an output stream or a file response, stream or spool the result instead of retaining multiple copies.

Android: converting HTML is a WebView print workflow

Android’s documented HTML approach loads content into a WebView, then obtains a print adapter. Use loadDataWithBaseURL() when relative images, CSS, or other local resources must resolve; use loadUrl() for a page address. Wait until the page has finished loading before creating the print job.

class HtmlPrintActivity : android.app.Activity() {
    private lateinit var webView: android.webkit.WebView

    override fun onCreate(state: android.os.Bundle?) {
        super.onCreate(state)
        webView = android.webkit.WebView(this)
        setContentView(webView)

        webView.settings.javaScriptEnabled = false // enable only when required
        webView.webViewClient = object : android.webkit.WebViewClient() {
            override fun onPageFinished(view: android.webkit.WebView, url: String) {
                val adapter = view.createPrintDocumentAdapter("html-document")
                val printManager = getSystemService(PRINT_SERVICE)
                    as android.print.PrintManager
                printManager.print(
                    "html-document",
                    adapter,
                    android.print.PrintAttributes.Builder()
                        .setMediaSize(android.print.PrintAttributes.MediaSize.ISO_A4)
                        .build()
                )
            }
        }

        val html = "<html><body><h1>Report</h1></body></html>"
        webView.loadDataWithBaseURL(
            "file:///android_asset/",
            html,
            "text/html",
            "UTF-8",
            null
        )
    }
}

This creates a print job; Android’s print services own the destination and lifecycle. The platform guide does not present it as a direct, synchronous ByteArray conversion. If your contract specifically requires bytes for upload or storage, use a JVM renderer in a service, or design a separate Android-native drawing pipeline.

When Android’s PdfDocument is the right tool

PdfDocument is suitable for charts, forms, and other content you can draw with Android canvas operations. It permits one page to be written at a time, is not thread safe, and writes the finished document to an output stream.

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.
fun nativePagesToBytes(): ByteArray {
    val document = android.graphics.pdf.PdfDocument()
    try {
        val pageInfo = android.graphics.pdf.PdfDocument.PageInfo
            .Builder(595, 842, 1) // points, approximately A4
            .create()
        val page = document.startPage(pageInfo)
        page.canvas.drawText("Native Android PDF", 40f, 60f,
            android.graphics.Paint().apply { textSize = 18f })
        document.finishPage(page)

        val output = java.io.ByteArrayOutputStream()
        document.writeTo(output)
        return output.toByteArray()
    } finally {
        document.close()
    }
}

This code demonstrates byte extraction, not HTML rendering. To reproduce an HTML layout with it, you would have to parse and measure the content yourself.

OpenHTMLtoPDF: a JVM alternative

OpenHTMLtoPDF is a pure-Java renderer that outputs PDF or images from a reasonable subset of well-formed XML/XHTML, some HTML5, and CSS 2.1 and later features. Its project explicitly warns that modern browser-oriented HTML5 pages may need adapted markup and styles. It is not a full browser engine, so test fonts, tables, pagination, SVG, and CSS features used by your templates.

Its project states that it is licensed under LGPL 2.1 or later and uses PDFBox. Verify the license obligations for the exact release and every distributed dependency with your legal or compliance team. The usual integration still follows the same Kotlin shape: configure the library’s builder with an HTML source and base URI, direct its PDF output to a ByteArrayOutputStream, and call toByteArray() after the builder completes. Use the release documentation for exact builder methods and imports rather than copying an API signature from another version.

Assets, CSS, fonts, and security

  • Base URI: Set one whenever HTML uses relative URLs. Without it, images and stylesheets commonly disappear.
  • Deterministic assets: Prefer packaged files or controlled HTTPS resources. Network-dependent assets can delay conversion or change between runs.
  • Fonts: Register or package fonts according to the renderer’s documentation. A browser font stack is not automatically available to a JVM renderer.
  • HTML quality: OpenHTMLtoPDF expects well-formed XHTML-like markup. Close tags, quote attributes, and simplify unsupported modern CSS.
  • Untrusted HTML: Apply an allowlist for external URLs and local file access. Do not let user content read arbitrary server files through a base URI or resource reference.
  • JavaScript: A PDF renderer is not equivalent to a browser executing an application. If the final HTML depends on client-side rendering, generate the finished markup first or use an appropriate browser-based capture service.

Testing and operational considerations

Validate the bytes, not just the array length

Check that the result begins with the PDF signature (%PDF-), can be opened by a PDF parser, and contains expected text or page count. A nonzero array can still contain an error page or truncated output if conversion failed upstream.

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

Control memory and concurrency

Use one conversion per request, close renderer/document resources, and avoid sharing mutable renderer objects between threads. Android’s PdfDocument is explicitly not thread safe. For large HTML, impose input and output limits, and queue work rather than allowing unbounded simultaneous conversions.

Make failures observable

Log the renderer version, template identifier, elapsed time, input size, and a sanitized resource-failure summary. Do not log credentials or private HTML. Keep the original exception as the cause so malformed markup and missing assets remain diagnosable.

Troubleshooting common failures

Symptom Likely cause Fix
Images or CSS are missing No base URI, inaccessible path, or blocked network request Set ConverterProperties.setBaseUri() (iText) or the equivalent builder base URI; verify the path from the conversion process.
Blank Android print preview Print adapter created before WebView finished loading Create the adapter from onPageFinished; ensure the WebView remains alive for the print job.
Modern page looks unlike Chrome JVM renderer supports only a documented HTML/CSS subset Simplify XHTML/CSS, replace unsupported layout, or choose a browser-based workflow.
ByteArray is empty or truncated toByteArray() called before conversion completed or an exception was ignored Call it only after the conversion method returns and propagate/log exceptions.
Out-of-memory errors Large HTML, images, or many concurrent in-memory arrays Resize assets, limit input, serialize jobs, and stream/spool output where your API permits.
Fonts differ between environments Font not installed or not registered on the runtime Package the font and configure the renderer; test on the same runtime image used in production.
Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Or skip the browser setup

If your actual need is a clean PDF or image of a public web page rather than server-side HTML template conversion, ScreenshotNeo provides a single HTTP request. Its cleanup step accepts cookie/consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each step can be disabled. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and response headers identify the page verdict and billing status. It also offers an MCP server with take_screenshot, get_page_info, and capture_pdf for Claude, Cursor, and other MCP clients.

See the complete parameter reference in the ScreenshotNeo documentation.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp

The same endpoint can return PNG, JPEG, WebP, or PDF according to the request options. It supports full-page capture with lazy images loaded, CSS-selector element capture, dark mode, device presets or custom viewports, retina scale, PDF paper and margin settings, custom CSS/JavaScript, clicks, waits, blocked resources, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, chosen cache TTLs, signed image links, asynchronous jobs with signed webhooks, bulk capture of up to 100 URLs per call, usage information, and an OpenAPI specification. Parameter names used by other screenshot APIs also work to ease migration.

There is a free allowance of 1,000 screenshots per month without a card. Paid plans start at $5 for 3,000 screenshots; every feature is included on every plan, and annual billing provides two months free. Create a free ScreenshotNeo account to try it.

How to choose

  • Choose Android WebView printing when the user needs Android’s print destinations and the source is HTML displayed in the app.
  • Choose PdfDocument when you control page drawing and need a native Android PDF byte stream.
  • Choose iText pdfHTML when a JVM service needs configurable HTML-to-PDF conversion and you have completed dependency and license review.
  • Choose OpenHTMLtoPDF when its documented HTML/CSS subset matches your templates and LGPL obligations are acceptable.
  • Choose a browser-based capture service when you need a rendered public web page, browser behavior, or a clean capture rather than a server-side template renderer.

Frequently Asked Questions

Can Android’s WebView print adapter return a PDF byte array directly?

The documented Android workflow creates a print job handled by the platform’s print services; it is not specified as a synchronous HTML-to-ByteArray function. Use a JVM renderer or a native PdfDocument pipeline when your API contract requires direct bytes.

Does PdfDocument convert an HTML string?

No. PdfDocument creates pages from native drawing operations. HTML layout must be handled separately.

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

Which renderer gives browser-identical HTML output?

Neither iText pdfHTML nor OpenHTMLtoPDF should be described as a full browser engine. Match your templates to the chosen renderer or use a browser-based workflow.

Is the Kotlin iText snippet version-independent?

No. The Java APIs are callable from Kotlin, but overloads, imports, and dependency coordinates can change. Verify them against the pdfHTML version selected for your project.

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.