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:
- Use
RenderMode.RASTERfor gradient-heavy pages - 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
- Best Practices – Avoid common pitfalls
- Supported Features – What works in each mode