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
@@ -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',