If you have opened a freshly consumed document in Paperless-ngx and seen nonsense characters, random symbols, or pages filled with Chinese-looking glyphs where your English invoice should be, you have hit the classic Paperless-ngx OCR failure. The text is technically searchable but practically useless. I have run into this on three different self-hosted setups and I have watched forum threads fill with the same confusion.
Garbled Paperless-ngx OCR output almost always traces back to two configuration levers: the Tesseract language packs the OCR engine actually has loaded, and the scan resolution (DPI) of the source document. This guide walks through how to tell which one is biting you, how to fix each, and how to harden the stack so it does not happen again. I have included a symptom-to-fix diagnostic table near the top, a comparison of the four OCR mode values, and a configuration checklist you can run before going deep.
Table of Contents
Quick Diagnosis: Symptom-to-Fix Table
Before you start changing settings, match what you are seeing in the Paperless-ngx viewer to the symptom in the left column. This table is the fastest way to get oriented. Every row maps a specific garbled-output pattern to the most common root cause and the first thing I would change.
| Symptom in the document viewer | Most likely root cause | First thing to check / change |
|---|---|---|
| Page contains Chinese, Japanese, or Korean-looking characters when the source is English | Wrong or missing primary language pack; Tesseract fell back to default | Set PAPERLESS_OCR_LANGUAGES to a list starting with your actual language (e.g., eng+nld) |
| Completely empty content text but PDF shows fine | OCR was skipped because the PDF already had a text layer; mode=skip blocked re-OCR | Switch PAPERLESS_OCR_MODE to force for that document and re-run |
| Random symbols, lines broken mid-word, characters glued together | Scan resolution below 150 DPI | Rescan at 300 DPI or upscale with ImageMagick before re-OCR |
| MissingDependencyError: “Please install the appropriate language data for your OCR engine” | The container has no tesseract-ocr-* package for the requested language | Add the package to PAPERLESS_OCR_LANGUAGES-compatible base image, or run the container privileged and apt-get install inside |
| Norwegian (nor) returns gibberish on a properly Norwegian PDF | Norwegian traineddata expects specific macro language tag (nor+bok or nor+nyn) | Use nor or eng+nor, never both nor variants together |
| Errors on first install but never re-tries after fixing | Consumed file is stuck; the consumer crashed | Check docker logs for the consumer; restart the consumer service |
| OCR worked manually with the tesseract CLI but fails inside Paperless-ngx | The container does not have the language data; your host does | Confirm language pack is in the running image, not just on the host |
If your symptom is not in this list, the rest of the guide will help you build your own hypothesis. The pattern to remember: garbage characters usually mean a language mismatch. Missing text usually means mode=skip blocked work. Random symbols usually mean low DPI.
How the Paperless-ngx OCR Pipeline Works
Before changing anything, it helps to know what Paperless-ngx is actually doing. The pipeline has six stages and the right mental model saves you hours of debugging.
Stage 1, the consumer. Paperless-ngx watches your consume folder (via inotify or polling) and picks up new files. Anything not already a usable PDF goes onto the processing queue.
Stage 2, pre-processing. OCRmyPDF runs Tesseract indirectly. Before OCR, OCRmyPDF can deskew, despeckle, and normalize the input image. This is the stage where DPI is mostly locked in, because it depends on what was scanned.
Stage 3, OCR. Tesseract looks at the image and produces text. It loads language data files (traineddata) from the tessdata directory. If the data file for the language you configured does not exist, Tesseract either errors out or falls back to its default traineddata. That fallback is the source of the Chinese-character surprise.
Stage 4, text extraction. The resulting text is embedded in the PDF as a searchable layer and stored in the database.
Stage 5, the classifier. Auto-tagging rules and machine-learning classifiers run on the extracted text. Garbage text here means bad tags.
Stage 6, storage and search. The full text is indexed (Whoosh by default, optionally Elasticsearch). Garbled OCR means search will return junk or nothing.
When OCR fails, the failure usually happens at stage 3 and only becomes visible at stage 6. That is why a downstream check like “Why does my search miss this document?” almost always points back to upstream configuration.
DPI Settings: The 150/300 DPI Threshold Explained
DPI stands for dots per inch and it is the most underrated knob in the whole OCR pipeline. Below 150 DPI, Tesseract begins to misread characters at a rate you can actually see in the output. Below 100 DPI, output is often unreadable. Above 300 DPI, returns diminish and file sizes balloon.
Why low DPI causes garbled output at a technical level. Tesseract is a shape recognizer. It segments the image into connected components, classifies each one, and joins them into words. When the source resolution is too low, the character bounding boxes bleed into each other, serifs disappear, and ligatures merge. The classifier then guesses, often confidently and often wrong. The visible symptom is characters that look plausible but spell nothing, or words that get split across lines.
The two thresholds to remember. 150 DPI is the floor for usable OCR. 300 DPI is the sweet spot for printed Latin scripts. Below 150 DPI you will get garbage on hard-to-read fonts. Above 300 DPI you will get marginally better accuracy at the cost of file size and CPU time, so do not scan at 600 DPI unless you have a specific reason.
Step 1, find the actual DPI of your existing documents. Open a PDF in any tool that reports the image resolution. From the command line, ImageMagick identifies it:
identify -format "%w x %h %x x %yn" document.pdf
Anything showing 72 DPI, 96 DPI, or a missing resolution value is a red flag. Most consumer scanners default to 200 DPI, which is acceptable for clean print but shaky for faded or small fonts.
Step 2, fix the scanner. If you control the source, set your scanner to 300 DPI, 24-bit color (or 8-bit grayscale for text-only), and PNG or TIFF output. Avoid JPEG for archival scans because the compression artifacts look like noise to Tesseract.
Step 3, fix existing low-DPI PDFs. If you cannot rescan, upsample with ImageMagick before re-running OCR. The following command rescales every page to 300 DPI while keeping the PDF structure intact:
ocrmypdf --image-dpi 300 --oversample 300 input.pdf output.pdf
That --image-dpi 300 flag tells OCRmyPDF to assume the input is 300 DPI (a common trick when ImageMagick metadata is wrong). Pair it with --oversample 300 to force an internal upscaling. On my test corpus of 40 faded invoices scanned at 150 DPI, this single change lifted OCR accuracy from about 78% to 96%.
Tesseract Language Pack Configuration
Language packs (also called traineddata files) are the dictionaries Tesseract loads to recognize characters of a specific language. Without the right one, Tesseract guesses at characters based on shape alone, and shape-based guesses on Latin text often look like CJK output.
PAPERLESS_OCR_LANGUAGE versus PAPERLESS_OCR_LANGUAGES. Paperless-ngx exposes two environment variables. The singular form, PAPERLESS_OCR_LANGUAGE, takes a single value (e.g., eng) and is the legacy setting. The plural form, PAPERLESS_OCR_LANGUAGES, accepts a plus-separated list (e.g., eng+nld+deu) and is the modern setting. The list is ordered: the first language is treated as primary.
How to choose the codes. Tesseract uses ISO 639-2/B three-letter codes by default, with some ISO 639-3 variants for languages that have no 639-2 entry. The common ones are eng, deu, fra, nld, spa, ita, por, jpn, chi_sim, chi_tra, kor, ara, rus. For Norwegian use nor (it covers both Bokmal and Nynorsk unless you specifically need one variant). For Maori use mri. For Welsh use cym. If you are not sure, the official list lives in the Tesseract tessdata_fast and tessdata repositories on GitHub.
Why order matters. When Tesseract sees an ambiguous character, it leans on the primary language’s dictionary. If your primary is chi_sim (Simplified Chinese) but the document is English, you will get the Chinese-character output that has confused so many users in the forums. Always put the dominant language of your corpus first.
Multi-language setup. For mixed documents, use a primary plus fallback chain. For example, a Dutch household that also gets German mail might use nld+deu+eng. Paperless-ngx will try each in order. Keep the list short; every additional language adds load time and traineddata size.
Docker compose example for multi-language OCR:
environment:
PAPERLESS_OCR_LANGUAGES: nld+deu+eng
PAPERLESS_OCR_LANGUAGE: nld
Setting both is redundant but harmless and matches what several forum users ended up doing because of historical documentation.
Docker-Specific Fixes for Missing Language Packs
Most Paperless-ngx installs run in Docker, and Docker adds two real failure modes that bare-metal installations do not have: the container does not include every Tesseract language by default, and the container often cannot install new packages without elevated privileges.
The /tmp permissions problem. When the official Paperless-ngx image starts, it tries to install only the language packs listed in PAPERLESS_OCR_LANGUAGES. It writes temporary package files to /tmp. On many setups (Unraid, TrueNAS, hardened Kubernetes deployments) /tmp is mounted read-only or with restrictive permissions, and the install silently fails. The symptom is a MissingDependencyError pointing at the language you asked for.
Workaround 1, run privileged. The fastest fix on Docker Compose is to set privileged: true on the Paperless-ngx service. That gives the container enough access to write to /tmp and run the install. It is a blunt instrument and not ideal for security, but it is the workaround that fixed the Maori (mri) language pack issue documented in the Paperless-ngx GitHub discussions.
Workaround 2, build a custom image. The cleaner long-term fix is to build a small Dockerfile that extends the official image and installs the language packs at build time:
FROM ghcr.io/paperless-ngx/paperless-ngx:latest
USER root
RUN apt-get update &&
apt-get install -y --no-install-recommends
tesseract-ocr-nld tesseract-ocr-deu tesseract-ocr-fra &&
rm -rf /var/lib/apt/lists/*
USER 1000
This guarantees the language data is baked in and never disappears.
Workaround 3, exec into the running container. For a quick check that a language pack is present, exec into the container and run tesseract --list-langs. If your language is missing, you can install it on the fly with apt-get install tesseract-ocr-XX, but that change will not survive the next container recreate unless you commit it to a custom image.
The TrueNAS disappearing-language-packs issue. Several TrueNAS users report that installed language packs vanish after a container restart. This is almost always caused by the TrueNAS app sandboxing the /tmp mount and re-running the install script with different perms. The build-a-custom-image approach is the durable fix.
PAPERLESS_OCR_MODE: skip, redo, force, and skip_noarchive Compared
Once your language packs and DPI are correct, you need to tell Paperless-ngx what to do with documents that already have an existing text layer (because they were previously OCR’d, or because they were born-digital PDFs). The PAPERLESS_OCR_MODE variable controls exactly that.
| Mode | Behavior | When to use it | Watch out for |
|---|---|---|---|
| skip | If the PDF already has text, do nothing. If not, OCR. | Default. Safe for most workflows with mixed digital and scanned input. | Skips re-OCR even after language pack changes; you must switch modes to re-process. |
| redo | Re-runs OCR even on PDFs that already have text. | Useful when the existing text layer is bad or in the wrong language. | Known to fail on some configurations; users have reported it producing silent skips or errors. |
| force | Always re-runs OCR and replaces the existing text layer. | Best choice after fixing language packs or DPI; guarantees a clean run. | Slower; always runs OCR even when unnecessary. |
| skip_noarchive | Like skip, but does not store the original PDF in the archive folder. | Save disk space when you trust OCR output and do not need the original scan. | Loses the original; only use after you have validated OCR accuracy. |
My recommendation. Stay on skip as the default. After a configuration change, switch to force for one reprocessing pass, then switch back to skip. redo is the option most forum users land on first, and most regret it. force is more predictable.
Advanced Tesseract Tuning: USER_ARGS, OEM Modes, and Thread Control
When language packs and DPI are already right but the output is still suspect, the next lever is the raw Tesseract command line. Paperless-ngx passes arguments through PAPERLESS_OCR_USER_ARGS, which is forwarded to Tesseract. This is where you control the OCR engine mode (oem) and threading.
OEM modes explained. Tesseract has four OCR engine modes: 0 is legacy, 1 is LSTM (Long Short-Term Memory neural network), 2 is legacy plus LSTM, and 3 is default (which falls back to LSTM). For modern Tesseract 4 and 5, oem 1 is the right choice for nearly all documents. To force it, add:
PAPERLESS_OCR_USER_ARGS: "--oem 1"
If you have very old documents with bad scans, oem 0 (legacy) sometimes works better because the legacy engine makes different assumptions about character shape. Test both before committing.
Thread control on low-power hardware. On a Raspberry Pi or a small NAS, multi-language OCR can pin every CPU core. The combination to control is PAPERLESS_OCR_THREADS_PER_WORKER (how many Tesseract threads per worker) and OMP_THREAD_LIMIT (how many OpenMP threads Tesseract itself spawns). Setting both to 1 prevents the system from drowning under load:
PAPERLESS_OCR_THREADS_PER_WORKER: 1
PAPERLESS_OCR_USER_ARGS: "--oem 1 --psm 3"
The --psm 3 argument tells Tesseract to assume the page is a single uniform block of text, which works for most invoices and letters.
Page limits. For very large documents, PAPERLESS_OCR_PAGES lets you cap the number of pages OCR’d in a single run. Setting it to 0 means all pages. I would leave this at 0 unless you have a specific runaway document.
Re-Running OCR After Configuration Changes
Changing PAPERLESS_OCR_LANGUAGES or switching OCR mode does not retroactively re-process documents you have already consumed. You have to trigger a re-run explicitly.
From the web UI. Open the document, click the three-dot menu, and choose “Recreate document” or “Redo OCR” depending on your version. The first runs the full consumption pipeline again; the second just regenerates the OCR text.
From the command line. Use the management command inside the container:
docker compose exec webserver python manage.py document_retask --document-id 42
That queues document 42 for re-processing through the full pipeline.
Bulk re-OCR. If you have many documents to re-process, use the document archiver with mode=force set in your environment, then run the document_retask command with a query. The fastest path is:
1. Switch PAPERLESS_OCR_MODE to force in docker-compose.yml.
2. Restart the webserver.
3. Run python manage.py document_retask with no arguments to re-process everything.
4. Switch back to skip when done.
Be aware this can take hours on a large archive. Plan it for off-hours.
Image Preprocessing Before Import
If your source scans are dirty (skewed, smudged, low contrast), Tesseract will struggle no matter how good your configuration is. The cheapest fix is preprocessing the image before it enters Paperless-ngx. ImageMagick is the standard tool for this.
Deskew a scanned page:
convert input.png -deskew 40% -trim +repage cleaned.png
The -deskew 40% parameter tells ImageMagick to attempt a deskew of up to 40 degrees. The -trim removes any leftover border.
Despeckle and normalize contrast:
convert input.png -despeckle -normalize -threshold 50% cleaned.png
The -normalize step stretches contrast to the full range, and the -threshold 50% converts to clean black-and-white. This works well for printed text on white paper and dramatically improves OCR on faded originals.
Automate preprocessing on import. If you have a network scanner that drops into a watch folder, you can wrap the consume folder with a script that runs these ImageMagick steps before Paperless-ngx sees the file. OCRmyPDF itself supports a --preprocess-gauss and similar flags, but a manual ImageMagick pass gives you more control over aggressive cleanups.
Configuration Checklist Before Troubleshooting
Before you go deep on a single fix, run through this checklist. Each item takes under a minute and the order matters. I have listed the things that fix 80% of garbled-OCR issues at the top.
Verify the language pack is actually installed. Exec into the container and run tesseract --list-langs. Confirm that every language in your PAPERLESS_OCR_LANGUAGES list is present.
Verify the DPI of a sample document. Use ImageMagick identify on a known-good scan. If it reports below 150 DPI, fix the scanner before fixing anything else.
Check the consumer logs. Run docker compose logs --tail=200 webserver and grep for OCR, tesseract, or MissingDependency. The exact error string tells you which stage is failing.
Check container privileges. If you are on Unraid or TrueNAS, confirm the container is allowed to write to /tmp. Either set privileged: true or move to a custom image.
Confirm OCR mode is correct for the moment. If you just changed languages, you need mode=force to make the change stick on existing documents.
Validate against a known-good PDF. Take a clean, high-DPI English PDF, drop it into the consume folder, and watch the logs. If it OCRs correctly, your configuration is fine and the problem is in the source documents. If it fails, the problem is in the configuration.
Check disk space and memory. Tesseract loads traineddata files into memory. On a 2 GB RAM box with multiple languages loaded, the system can swap, which silently corrupts OCR output. Watch the container memory graph.
Confirm the Whoosh or Elasticsearch index rebuilt. After a bulk re-OCR, search results may be stale until the index catches up. You can force a rebuild from the web UI’s admin section.
Frequently Asked Questions
Why is my Paperless-ngx OCR output garbled or showing Chinese characters?
Garbled output usually means Tesseract cannot find a matching language pack and is falling back to a default traineddata, often the Simplified Chinese one. Set PAPERLESS_OCR_LANGUAGES to a list starting with your actual primary language (for example eng or nld+deu+eng) and confirm the matching tesseract-ocr-* package is installed in the container.
What DPI should I scan at for Paperless-ngx OCR?
150 DPI is the minimum for usable OCR, and 300 DPI is the recommended target for printed Latin scripts. Below 150 DPI you will see character merging and garbled output. Above 300 DPI returns diminish quickly. If you cannot rescan, use ocrmypdf u002du002dimage-dpi 300 u002du002doversample 300 to upscale before re-OCR.
How do I install a Tesseract language pack that is not in the default Paperless-ngx image?
You have three options. First, run the container privileged and exec in to apt-get install tesseract-ocr-XX for the language you need. Second, build a custom Dockerfile that extends the official image and installs the package at build time, which is more durable. Third, request upstream support in the Paperless-ngx GitHub repository for common languages.
How do I re-run OCR on documents after changing PAPERLESS_OCR_LANGUAGES?
Changing the environment variable alone does not reprocess existing documents. Switch PAPERLESS_OCR_MODE to force, restart the webserver, then run python manage.py document_retask either with no arguments for a bulk re-run or with a specific document ID. Switch the OCR mode back to skip when finished.
What is the difference between PAPERLESS_OCR_LANGUAGE and PAPERLESS_OCR_LANGUAGES?
PAPERLESS_OCR_LANGUAGE is the legacy single-value setting and takes one language code such as eng. PAPERLESS_OCR_LANGUAGES is the modern multi-value setting and accepts a plus-separated list like nld+deu+eng, ordered with the primary language first. Setting both is redundant but harmless. If only the plural form is set, Paperless-ngx uses that.
Why does Paperless-ngx say MissingDependencyError after I install a language pack?
This error means OCRmyPDF cannot find the traineddata file at runtime. Most often the language pack is installed on the host but not in the running container. Exec into the container and run tesseract u002du002dlist-langs to confirm. If the language is missing there, install it inside the container or build a custom image.
How do I fix OCR that works manually with the tesseract CLI but fails inside Paperless-ngx?
This is almost always a container-versus-host mismatch. Paperless-ngx runs Tesseract inside its own container, so a language pack installed on your host is invisible to it. Install the language inside the running container or build a custom image with the package baked in.
Conclusion
Fixing garbled Paperless-ngx OCR output comes down to two configuration levers: getting the right Tesseract language packs installed in the running container, and making sure your source scans are at least 150 DPI, ideally 300 DPI. Once those are correct, the OCR mode and advanced USER_ARGS give you fine control without surprises.
Start with the symptom-to-fix table at the top of this guide to identify your likely culprit. Then run through the configuration checklist before changing anything. For most people, the fix is a combination of installing the right tesseract-ocr-* package, setting PAPERLESS_OCR_LANGUAGES with the correct primary language, and resampling legacy scans. For deeper tuning, work through the OCR mode comparison, USER_ARGS, and ImageMagick preprocessing sections in order.