# text_renderer turns a corpus and a font folder into training images for OCR

> text_renderer generates text line images for training deep learning OCR models, with a modular design where corpus, effect and layout are separate components and augmentation comes from Albumentations. It also writes labels and sizes alongside the images, and can store the dataset in lmdb for PaddleOCR.

**oh-my-ocr/text_renderer** — Generate text line images for training deep learning OCR models

- Repository: https://github.com/oh-my-ocr/text_renderer
- Website: https://oh-my-ocr.github.io/text_renderer/
- Stars: 914 · Forks: 174
- Language: Python
- License: MIT
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/oh-my-ocr-text-renderer

## The output is a text line, not a document

text_renderer generates text line images for training deep learning OCR models, and the README points at CRNN as an example of the kind of model it feeds.

That word, line, defines the scope precisely. This is not a document renderer and not a full page layout engine. It takes a piece of text, sets it in a font at a given size, places it on a background, and applies distortions, and then it writes the image out with the label and the size recorded alongside.

For anyone training a recogniser, that is exactly the right shape. A CRNN style model consumes cropped line images with a transcription attached. Synthetic line generation solves a specific and common problem: you have a language, a domain and a character set, and you need far more examples than real photographs can supply, or real photographs of one business cannot cover the variations.

The README is explicit about the corpus requirement, and it makes two points that determine whether this tool works for you. The corpus must be in the target language for which you want to perform OCR recognition. And the corpus should meet your actual business needs, such as the education field or the medical field.

So the input is not a bag of random words. It is a text corpus that represents what your model will have to read.

The font side has the same specificity. Font files can be .ttf, .otf or .ttc, and background images can be of any size, either from your business scenario or from publicly available datasets such as COCO or VOC.

## Three components and three effect stages

The feature list leads with a modular design, and that is the thing to understand before reading any configuration.

You can easily add different components, and the three named in the README are Corpus, Effect and Layout, each with its own documentation page. Corpus handles where the text comes from and how it is sampled. Effect handles the distortions applied to the rendered result. Layout handles where multiple pieces of text sit relative to each other on the image.

That third point is what makes the tool more than a text-on-background generator. It supports rendering multiple corpora on one image with different effects, and Layout is responsible for the arrangement between them. A single image can therefore contain several text elements, each drawn from a different corpus, each with its own effect applied.

The second feature is the integration with Albumentations for image augmentation effects. That matters because Albumentations is a well-known augmentation library, and reusing it means the augmentation pipeline is the same one you would use for the rest of your training data.

The third feature is the most interesting, and it is easy to miss. Effects can be applied at different stages of the rendering process, and there are three named stages: corpus_effects, layout_effects and render_effects. All three are attributes of RenderCfg.

The distinction is not cosmetic. An effect applied at the corpus stage affects the text before it is placed, an effect at the layout stage affects how the text elements are arranged on the canvas, and an effect at the render stage affects the finished image. Choosing the wrong stage produces output that looks wrong in a way that is hard to diagnose from the image alone, so the documentation page for RenderCfg is the one to read first.

The remaining features are vertical text generation, lmdb dataset output compatible with PaddleOCR, and a web font viewer.

## Installing with uv, and running the bundled example

The prerequisites are Python 3.10 or newer and Linux, macOS or Windows.

Installation goes through uv, and the extras are named explicitly.

```bash
git clone https://github.com/oh-my-ocr/text_renderer
cd text_renderer
uv sync                              # core dependencies
uv sync --extra tools                # + font viewer and utility scripts
uv sync --extra docs                 # + Sphinx for building documentation
uv sync --all-extras                 # everything
```

So the core install is the first line, and the optional extras pull in the font viewer and utility scripts, the Sphinx documentation build, or everything.

The example run is a single command that takes a config, an output format and a process count.

```bash
uv run python main.py \
    --config example_data/example.py \
    --dataset img \
    --num_processes 2 \
    --log_period 10
```

The README adds a Windows note, because the line continuation character differs. Use ^ instead of \ for line continuation, or write the command on a single line.

```bash
uv run python main.py --config example_data/example.py --dataset img --num_processes 2 --log_period 10
```

The generated data goes to the example_data/output directory, and a labels.json file contains all the annotations.

```json
{
  "labels": {
    "000000000": "test",
    "000000001": "text2"
  },
  "sizes": {
    "000000000": [
      120,
      32 
    ],
    "000000001": [
      128,
      32 
    ]
  },
  "num-samples": 2
}
```

The sizes entry is the part that matters most for training. A recogniser needs to know the height of the text line as well as the transcription, and recording the rendered size per sample means you can filter or normalise later without re-rendering.

## lmdb output for PaddleOCR, and the keys it writes

The default output format writes images to disk. The alternative, and the reason the lmdb dependency is in the manifest, is the lmdb dataset format compatible with PaddleOCR.

You can use --dataset lmdb to store images in an lmdb file, and the file contains the following keys.

```
num-samples
image-000000000
label-000000000
size-000000000
```

The naming is regular: each sample gets a zero-padded numeric identifier, and the image, the label and the size are stored under that identifier with a prefix. The separate num-samples key is the count.

Why this matters is scale. A synthetic dataset for a recogniser is routinely hundreds of thousands of samples, and hundreds of thousands of small files on a filesystem is a slow way to read them repeatedly during training. A single key-value store with an index is the format PaddleOCR expects, and it means the training pipeline reads from a memory-mapped database rather than walking a directory.

The tradeoff is that the output is no longer browsable as files. If you want to look at a few hundred rendered images to sanity check the configuration, the img format is easier. If you are about to generate a real training set, lmdb is the one to pick.

The label and the size travel with the image in both formats, so the annotation quality does not depend on the choice.

## The config file is the interface, and it is Python

Configuration is a Python file, not a data format. A config.py file in the workspace directory must have a configs variable, which is a list of GeneratorCfg objects.

```python
import os
from pathlib import Path

from text_renderer.effect import *
from text_renderer.corpus import *
from text_renderer.config import (
    RenderCfg,
    NormPerspectiveTransformCfg,
    GeneratorCfg,
    SimpleTextColorCfg,
)

CURRENT_DIR = Path(os.path.abspath(os.path.dirname(__file__)))
```

The rendering settings live inside a RenderCfg, and the example shows every important knob at once.

```python
            bg_dir=CURRENT_DIR / "bg",
            height=32,
            perspective_transform=NormPerspectiveTransformCfg(20, 20, 1.5),
            corpus=WordCorpus(
                WordCorpusCfg(
                    text_paths=[CURRENT_DIR / "corpus" / "eng_text.txt"],
                    font_dir=CURRENT_DIR / "font",
                    font_size=(20, 30),
                    num_word=(2, 3),
                ),
            ),
            corpus_effects=Effects(Line(0.9, thickness=(2, 5))),
            gray=False,
            text_color_cfg=SimpleTextColorCfg(),
```

Read that as a description of the generated sample. The background directory is bg, the text line height is fixed at 32 pixels, and a perspective transform of 20, 20 and 1.5 is applied to the image. The corpus is a WordCorpus built from a text file, with fonts drawn from the font directory, a font size range of 20 to 30, and two or three words sampled per image.

Three things follow from the README's own explanation of the example. It specifies the location of the resource files. It specifies the text sampling method, with 2 or 3 words randomly selected from the corpus. And it configures effects for generation.

The word count matters more than it looks. An OCR model trained on two-word lines will not perform well on a single long line, and the sampling parameters are how you control the shape of the data your model sees.

## The charset file is the one setting that affects your accuracy

Most of the resource preparation is obvious: a font directory, a background directory, a text corpus. One option is described as optional but recommended, and it is the one that changes how well your trained model will work.

The charset file. OCR models in real-world scenarios, for example CRNN, usually support only a limited character set, so it is better to filter out characters outside the character set during data generation. You do that by setting the chars_file parameter.

Think about what happens without it. Your corpus contains a hundred thousand distinct characters, and the model can only ever emit the forty or so it was designed to recognise. Every sample containing a character outside that set is training the model to output something it will never be allowed to predict, and the samples that matter, the ones made entirely of valid characters, are a smaller fraction of the data than you think.

Filtering at generation time rather than after is the right place to do it. The generator can simply not produce the offending samples, so you spend compute on useful data.

The corpus preparation guidance follows the same logic from the other direction. The corpus must be in the target language for the recognition task, and it should meet your actual business needs, such as the education field or the medical field. If your recogniser will read medical records, a corpus of movie subtitles teaches it the wrong vocabulary.

For a first run, the README offers three pre-prepared resources: simsun.ttf, background.png and eng_text.txt, and shows the directory layout they go in, with bg, corpus and font folders under a workspace directory.

## Where text_renderer is the wrong tool

Three cases rule it out, and they are all about what the output does not contain.

The first is document understanding. text_renderer produces text line images. If your task is reading a page, you need page images with reading order, and text_renderer gives you isolated lines with no page structure, no columns and no tables. A layout analysis model trained on this data learns to read lines, not documents.

The second is a project that needs balanced character coverage without extra work. The feature list has an unchecked item for a corpus sampler, described as helpful to perform character balance. So the tool can sample from a corpus, but balancing the character distribution across the generated set is not something it does for you. You would need to prepare a corpus that is already balanced, or post-process, and for a rare character in a large corpus that means a lot of generated images to get a reasonable number of examples.

The third is production data realism. This is synthetic rendering, so the images are as realistic as the fonts and backgrounds you supply. If your deployment domain is handwriting, receipts, or camera-captured storefront text, the gap between synthetic line images and real photographs is large, and the training data has to be supplemented with real samples regardless.

There is also a maintenance fact to weigh. The repository is not archived and the last push was on 2026-05-17, so the code is current, but the listed release history has a single entry, v0.2.0 from 2026-03-29, and the project version in pyproject.toml also reads 0.2.0. A version in the zero series with one published release tells you the API is still moving.

The alternative for many teams is to use the dataset formats they already train with and write a thin generator, or to use a general document renderer to produce full pages and crop them. The reason to prefer text_renderer is that it is purpose built for the line-recognition case, with the layout component, the effect stages and the lmdb output already solved.

## Conclusion

Adopt text_renderer if you are training an OCR model and need synthetic line images that match your domain, because the corpus, effect and layout components let you control what the training data looks like rather than accepting whatever a generic tool produces. Do not adopt it if you need document images with reading order, tables or free layout, since the output is text lines, and note that the corpus sampler for character balance is still an open item on the project's own feature list. Before you start, read the config documentation for RenderCfg, because the effects system has three separate stages and choosing the wrong one changes the output.

## FAQ

### What is text_renderer used for?

It generates text line images for training deep learning OCR models, for example CRNN. You supply a font, a background and a text corpus, and it renders labelled line images you can train on.

### How do I install text_renderer?

Clone the repository and use uv. Run uv sync for the core dependencies, uv sync --extra tools to add the font viewer and utility scripts, uv sync --extra docs for Sphinx, or uv sync --all-extras for everything.

### What does the labels.json file contain?

It contains a labels object mapping the sample identifier to its text, a sizes object mapping the identifier to the rendered width and height, and a num-samples entry with the count of generated samples.

### Can text_renderer write an lmdb dataset for PaddleOCR?

Yes. Use --dataset lmdb instead of --dataset img, and the output lmdb file contains the keys num-samples, and for each sample an image, a label and a size entry keyed by the sample identifier.

### What is the chars_file parameter for?

The README describes the charset file as optional but recommended. OCR models usually support only a limited character set, so setting chars_file filters out characters outside that set during data generation rather than after.

### At what stages can effects be applied in text_renderer?

Effects can be applied at three stages of the rendering process, configured on RenderCfg: corpus_effects, layout_effects and render_effects.

## Sources

- [License: MIT](https://github.com/oh-my-ocr/text_renderer/blob/master/LICENSE)
- [oh-my-ocr/text_renderer on GitHub](https://github.com/oh-my-ocr/text_renderer)
- [Project website](https://oh-my-ocr.github.io/text_renderer/)
- [README](https://github.com/oh-my-ocr/text_renderer/blob/master/README.md)
- [Releases](https://github.com/oh-my-ocr/text_renderer/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/oh-my-ocr-text-renderer
