Troubleshooting

Common issues and how to fix them.


Non-uniform rounded corners render identically

Problem: All corners of a RoundedCornerShape look the same even though you specified different radii.

Cause: Skia’s SVGCanvas serializes rounded rects as <rect rx ry> which only supports a single corner radius.

Fix: Use PdfRoundedCornerShape for non-uniform corners:

// Instead of:
Modifier.clip(RoundedCornerShape(topStart = 24.dp, bottomEnd = 24.dp))

// Use:
Modifier.clip(PdfRoundedCornerShape(topStart = 24.dp, bottomEnd = 24.dp))

Text is not selectable in the PDF

Problem: Text appears as an image and cannot be selected or searched.

Cause: The PDF was generated with RenderMode.RASTER.

Fix: Use RenderMode.VECTOR (the default):

renderToPdf(mode = RenderMode.VECTOR) { /* content */ }

Letters look squashed / spacing is uneven

Problem: Some letter pairs render with no space between them, or spacing looks subtly uneven (e.g. 2.5% ($34.69) showing as 2.5%($34.69)).

Cause: The text was drawn with a substituted font whose glyph widths differ from the font Compose laid the text out with. Since v1.3.1 the renderer embeds the exact shaping fonts automatically, so this only happens when a font genuinely can’t be embedded — check the logs for a FontResolver warning naming the family.

Fix: Make the shaping font embeddable — usually by declaring it from static font files via Font(resource = ...). The most common remaining trigger is bold text in a variable-only system font (e.g. macOS .SF NS): declare an explicit FontFamily with a static bold file instead. Substituted glyphs are compressed so they can never overlap, but only the real font gives exact output.


Bold or italic text renders in a fallback font

Problem: Regular text embeds correctly, but bold/italic falls back to a compressed standard font (the FontResolver warning names the family).

Cause: PDFBox cannot instantiate variable-font axes. A variable font’s default (regular) instance embeds fine, but a bold/italic instance of a variable-only family cannot be embedded faithfully.

Fix: Declare each weight/style from a static file:

val brand = FontFamily(
    Font(resource = "fonts/Brand-Regular.ttf", weight = FontWeight.Normal),
    Font(resource = "fonts/Brand-Bold.ttf", weight = FontWeight.Bold),
)

Most families (Inter, Roboto, etc.) distribute static variants alongside variable ones — use Inter-Regular.ttf/Inter-Bold.ttf rather than Inter-Variable.ttf.


PDF file size is too large

Problem: Generated PDFs are several megabytes.

Possible causes and fixes:

Cause Fix
Using RenderMode.RASTER Switch to RenderMode.VECTOR
High density in raster mode Lower Density (e.g., 2f instead of 4f)
Large embedded images Resize images before rendering
Many pages with images Each page embeds its own image data

Vector mode typically produces 10-100 KB files. Raster mode at Density(3f) on A4 can exceed 5 MB per page.


Gradients don’t appear in vector mode

Problem: Gradient backgrounds render as flat colors or bitmapped sections.

Cause: Skia’s SVGCanvas does not emit gradient definitions in a format the converter processes. Gradients are rasterized by Skia before reaching the SVG output.

Fix options:

  1. Use RenderMode.RASTER for gradient-heavy pages
  2. Simulate gradients with thin colored strips:
    Row(Modifier.fillMaxWidth().height(60.dp)) {
        for (i in 0 until 50) {
            val fraction = i / 50f
            val color = lerp(Color.Blue, Color.Red, fraction)
            Box(Modifier.weight(1f).fillMaxHeight().background(color))
        }
    }
    

Content overflows the page

Problem: Content extends beyond the visible page area or is clipped.

Cause: Compose does not auto-paginate. The content area is fixed at config.contentWidth x config.contentHeight.

Fix: Split content across pages manually:

val itemsPerPage = 20
val pageCount = (items.size + itemsPerPage - 1) / itemsPerPage

renderToPdf(pages = pageCount) { pageIndex ->
    val pageItems = items.drop(pageIndex * itemsPerPage).take(itemsPerPage)
    Column { pageItems.forEach { Text(it) } }
}

Headless rendering on Linux CI

Problem: renderToPdf fails on a headless Linux server (CI) with display-related errors.

Cause: Compose Desktop requires a display server (X11) even for offscreen rendering.

Fix: Use xvfb-run to provide a virtual framebuffer:

# GitHub Actions example
- name: Run tests
  run: xvfb-run ./gradlew :compose2pdf:test

For Docker, install xvfb and wrap the command:

apt-get install -y xvfb
xvfb-run ./gradlew :compose2pdf:test

Compose2PdfException during rendering

Problem: A Compose2PdfException is thrown.

Fix: Check the cause for the underlying error:

try {
    renderToPdf { content() }
} catch (e: Compose2PdfException) {
    println("Rendering failed: ${e.message}")
    e.cause?.printStackTrace()
}

Common causes:

  • Invalid font file (corrupted or variable font)
  • Out of memory for very large pages at high density
  • Compose layout errors in the content lambda

@InternalComposeUiApi warnings

Problem: Compiler warnings about @InternalComposeUiApi usage.

Cause: The library uses CanvasLayersComposeScene which is an internal Compose API. No public alternative exists.

Fix: This is expected. The opt-in is configured in the library’s build.gradle.kts. If you see warnings in your own build, they come from the library dependency and can be safely ignored.


See also