feat: anti-alias Ghostscript rasterization to improve OCR quality

Ghostscript 10.x renders aliased glyphs that OCR frequently misreads as
extra word breaks or substituted characters. Enable text and graphics
anti-aliasing (-dTextAlphaBits=4 -dGraphicsAlphaBits=4) for the contone
raster devices, which empirically improves OCR accuracy on the
Ghostscript path, especially for small fonts at moderate DPI. The 1-bit
mono devices are excluded, since older Ghostscript rejects alpha bits on
them and pngmonod performs its own anti-aliased downscaling.

Also log which rasterizer rendered each page at debug verbosity and
clarify the --rasterizer help text, so quality reports are easier to
diagnose. The default rasterizer (auto) already prefers pypdfium2, which
anti-aliases; this primarily benefits --rasterizer ghostscript and
installs without pypdfium2.

Closes #1439.
This commit is contained in:
James R. Barlow
2026-06-10 23:39:03 -07:00
parent 58642d8411
commit 11de13ecfe
6 changed files with 95 additions and 3 deletions
+14
View File
@@ -62,6 +62,20 @@
better input for OCR on faint or anti-aliased scans at negligible cost and
no change to output file size, since the rasterized image is an
intermediate that is discarded after OCR.
- When rasterizing pages with Ghostscript, OCRmyPDF now enables text and
graphics anti-aliasing (``-dTextAlphaBits=4 -dGraphicsAlphaBits=4``) for the
grayscale and color raster devices. Ghostscript 10.x renders aliased glyphs
that OCR frequently misreads as extra word breaks or substituted characters;
anti-aliasing materially improves OCR accuracy on the Ghostscript
rasterization path, especially for small fonts at moderate resolution. The
1-bit monochrome devices are unaffected, since they perform their own
anti-aliased downscaling and older Ghostscript versions reject alpha-bit
options on them. Note that the default rasterizer (``--rasterizer auto``)
prefers pypdfium2, which already anti-aliases; this change benefits users who
select ``--rasterizer ghostscript`` or do not have pypdfium2 installed.
OCRmyPDF now also logs which rasterizer rendered each page at debug verbosity
(``-v 1``), and the ``--rasterizer`` help text explains the OCR-quality
trade-off, to make such reports easier to diagnose. {issue}`1439`
## v17.5.0
+14
View File
@@ -150,6 +150,19 @@ def rasterize_pdf(
else:
effective_dpi = raster_dpi
# Anti-alias text and vector graphics when rendering to a contone device.
# Ghostscript 10.x renders aliased glyphs that OCR frequently misreads as
# extra word breaks; anti-aliasing empirically improves OCR accuracy on the
# Ghostscript path, especially for small fonts at moderate DPI (#1439).
# The 1-bit mono devices do not accept alpha bits (older Ghostscript
# rejects them) and pngmonod performs its own anti-aliased downscaling.
mono_devices = (GhostscriptRasterDevice.PNGMONO, GhostscriptRasterDevice.PNGMONOD)
antialias_args = (
[]
if raster_device in mono_devices
else ['-dTextAlphaBits=4', '-dGraphicsAlphaBits=4']
)
args_gs = (
[
GS,
@@ -162,6 +175,7 @@ def rasterize_pdf(
f'-dLastPage={pageno}',
f'-r{effective_dpi.x:f}x{effective_dpi.y:f}',
]
+ antialias_args
+ (['-dUseCropBox'] if use_cropbox else [])
+ (['-dFILTERVECTOR'] if filter_vector else [])
+ (['-dPDFSTOPONERROR'] if stop_on_error else [])
@@ -256,6 +256,8 @@ def rasterize_pdf_page(
# Let pypdfium handle it (it will error in check_options if unavailable)
return None
log.debug("Rasterizing page %d with the Ghostscript rasterizer", pageno)
ghostscript.rasterize_pdf(
input_file,
output_file,
+2
View File
@@ -255,6 +255,8 @@ def rasterize_pdf_page(
if pdfium is None:
return None # Fall back to Ghostscript
log.debug("Rasterizing page %d with the pypdfium2 rasterizer", pageno)
# Acquire lock to ensure thread-safe access to pypdfium2
with (
_pdfium_lock,
+7 -3
View File
@@ -423,9 +423,13 @@ Online documentation is located at:
'--rasterizer',
choices=['auto', 'ghostscript', 'pypdfium'],
default='auto',
help="Choose PDF page rasterizer. 'auto' prefers pypdfium when available, "
"falling back to Ghostscript. 'pypdfium' is faster but requires the "
"pypdfium2 package. 'ghostscript' uses the traditional Ghostscript rasterizer.",
help="Choose PDF page rasterizer. 'auto' (the default) prefers pypdfium2 "
"when the pypdfium2 package is installed, falling back to Ghostscript "
"otherwise. pypdfium2 anti-aliases page content and generally produces "
"better input for OCR than Ghostscript 10.x, which can render aliased "
"glyphs that OCR misreads as extra word breaks. 'pypdfium' forces the "
"pypdfium2 rasterizer (requires the pypdfium2 package); 'ghostscript' "
"forces the traditional Ghostscript rasterizer.",
)
advanced.add_argument(
'--rotate-pages-threshold',
+56
View File
@@ -141,6 +141,62 @@ def test_rasterize_low_dpi_one_axis(francais, outdir):
assert im.info['dpi'] == forced_dpi
def _capture_rasterize_args(resources, outdir, raster_device):
"""Run rasterize_pdf with the gs subprocess mocked; return the gs argv."""
out = outdir / 'out.png'
captured = {}
def fake_run(args, **kwargs):
captured['args'] = list(args)
# Produce a valid PNG so rasterize_pdf's post-processing succeeds.
Image.new('RGB', (2, 2)).save(out)
return subprocess.CompletedProcess(args, returncode=0, stdout=b'', stderr=b'')
with patch('ocrmypdf._exec.ghostscript.run', side_effect=fake_run):
rasterize_pdf(
resources / 'francais.pdf',
out,
raster_device=raster_device,
raster_dpi=Resolution(150.0, 150.0),
)
return captured['args']
@pytest.mark.parametrize(
'raster_device',
[
GhostscriptRasterDevice.PNGGRAY,
GhostscriptRasterDevice.PNG256,
GhostscriptRasterDevice.PNG16M,
],
)
def test_rasterize_antialiases_contone_devices(resources, outdir, raster_device):
"""Contone raster devices receive anti-aliasing flags to aid OCR.
Ghostscript 10.x renders aliased glyphs that OCR misreads as extra word
breaks; -dTextAlphaBits/-dGraphicsAlphaBits markedly improve accuracy,
especially for small fonts at moderate DPI (see issue #1439).
"""
args = _capture_rasterize_args(resources, outdir, raster_device)
assert '-dTextAlphaBits=4' in args
assert '-dGraphicsAlphaBits=4' in args
@pytest.mark.parametrize(
'raster_device',
[GhostscriptRasterDevice.PNGMONO, GhostscriptRasterDevice.PNGMONOD],
)
def test_rasterize_no_antialias_on_mono_devices(resources, outdir, raster_device):
"""1-bit mono devices must not receive alpha-bit flags.
Older Ghostscript versions reject -dTextAlphaBits on 1-bit devices, and
pngmonod performs its own anti-aliased downscaling.
"""
args = _capture_rasterize_args(resources, outdir, raster_device)
assert not any(a.startswith('-dTextAlphaBits') for a in args)
assert not any(a.startswith('-dGraphicsAlphaBits') for a in args)
def test_generate_pdfa_default_jpeg_quality(outdir):
"""When jpeg_quality is None, Ghostscript receives -dJPEGQ=95 (default)."""
with (