Compare commits

...
7 Commits
Author SHA1 Message Date
James R. Barlow 4e974ebd46 Bump version: v17.4.0 2026-03-21 01:43:13 -07:00
James R. Barlow 6f2b8408c1 v17.4.0 release notes 2026-03-21 01:43:03 -07:00
James R. Barlow 1dba941261 Add cyclopts for dev 2026-03-21 01:37:48 -07:00
James R. Barlow ef76625abb Fix text stretching in fpdf2 renderer for widely-spaced words
The inter-word Tz calculation stretched "word " to span from the current
word to the next, producing extreme horizontal scaling (300-500%) for
words far apart (e.g. in tables). Use per-word Tz instead — Td
positioning already handles inter-word gaps correctly.

Fixes #1635
2026-03-16 16:00:00 -07:00
James R. Barlow 57bb554a70 Fix verapdf NotADirectoryError crash on some platforms
Catch OSError (parent of both FileNotFoundError and
NotADirectoryError) in verapdf.available() so environments where
executing `verapdf` raises NotADirectoryError gracefully fall back
instead of crashing the pipeline. Fixes #1638.
2026-03-10 02:08:59 -07:00
James R. Barlow 5b9d6f979e Add --no-overwrite / -n option to prevent overwriting output files
Fixes #1642. Adds an early check in check_requested_output_file() that
raises OutputFileAccessError (exit code 5) if the destination file
already exists and --no-overwrite is set. The option is wired through
CLI, OcrOptions, and the Python API.
2026-03-10 01:58:57 -07:00
James R. Barlow b588e3bfd7 Fix optimize=2/3 crash when using Python API
The jpg_quality and png_quality options default to None in the pydantic
model, but the fallback check only handled == 0. This caused a TypeError
when calling ocrmypdf.ocr() with optimize >= 2 without explicitly
setting quality values. Fixes #1641.
2026-03-10 01:51:07 -07:00
11 changed files with 62 additions and 19 deletions
+14
View File
@@ -3,6 +3,20 @@
# v17
## v17.4.0
- Added ``--no-overwrite`` / ``-n`` option to prevent overwriting output files.
If the destination file already exists, OCRmyPDF exits with code 5
(``OutputFileAccessError``). {issue}`1642`
- Fixed text layer stretching in the fpdf2 renderer for widely-spaced words.
The horizontal scaling (Tz) was incorrectly stretched to fill inter-word gaps
instead of relying on Td positioning, causing text selection to highlight far
beyond the actual word boundaries. {issue}`1635`
- Fixed ``optimize=2`` or ``optimize=3`` crash when using the Python API without
explicitly setting ``jpg_quality`` or ``png_quality``. {issue}`1641`
- Fixed ``verapdf`` availability check crashing with ``NotADirectoryError`` on
some platforms. {issue}`1638`
## v17.3.0
- Fixed Python API ignoring the ``language`` parameter, always defaulting to
+7 -2
View File
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
[project]
name = "ocrmypdf"
version = "17.3.0"
version = "17.4.0"
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
readme = "README.md"
license = "MPL-2.0"
@@ -156,7 +156,12 @@ quote-style = "preserve"
[dependency-groups]
# Developer-only tools - use `uv sync --group <name>`
dev = ["mypy>=1.13.0", "ipykernel>=6.29.5", "reportlab>=4.4.4"]
dev = [
"mypy>=1.13.0",
"ipykernel>=6.29.5",
"reportlab>=4.4.4",
"cyclopts>=4.5.1",
]
test = [
# Core testing framework
"coverage[toml]>=6.2",
+1 -1
View File
@@ -36,7 +36,7 @@ def available() -> bool:
"""Check if verapdf is available."""
try:
version()
except MissingDependencyError:
except (MissingDependencyError, OSError):
return False
return True
+3
View File
@@ -188,6 +188,9 @@ class OcrOptions(BaseModel):
"""Compatibility alias for jpg_quality."""
self.jpg_quality = value
# Output behavior
no_overwrite: bool = False
# Advanced options
max_image_mpixels: float = 250.0
pdf_renderer: str = 'auto'
+11
View File
@@ -217,6 +217,17 @@ def check_requested_output_file(options: OcrOptions) -> None:
f"Output file location ({options.output_file}) is not a writable file."
)
if (
options.no_overwrite
and not hasattr(options.output_file, 'writable')
and options.output_file != '-'
and Path(str(options.output_file)).exists()
):
raise OutputFileAccessError(
f"Output file already exists: {options.output_file}\n"
"To overwrite it, omit the --no-overwrite / -n option."
)
def report_output_file_size(
options: OcrOptions,
+1 -1
View File
@@ -1,3 +1,3 @@
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
__version__ = "17.3.0"
__version__ = "17.4.0"
+2
View File
@@ -440,6 +440,7 @@ def ocr(
continue_on_soft_render_error: bool | None = None,
invalidate_digital_signatures: bool | None = None,
tagged_pdf_mode: str | None = None,
no_overwrite: bool | None = None,
plugins: Iterable[Path | str] | None = None,
plugin_manager: OcrmypdfPluginManager | None = None,
keep_temporary_files: bool | None = None,
@@ -502,6 +503,7 @@ def ocr( # noqa: D417
continue_on_soft_render_error: bool | None = None,
invalidate_digital_signatures: bool | None = None,
tagged_pdf_mode: str | None = None,
no_overwrite: bool | None = None,
plugins: Iterable[Path | str] | None = None,
plugin_manager: OcrmypdfPluginManager | None = None,
keep_temporary_files: bool | None = None,
+12 -2
View File
@@ -137,8 +137,9 @@ Online documentation is located at:
'output_file',
metavar="output_pdf",
help="Output searchable PDF file (or '-' to write to standard output). "
"Existing files will be overwritten. If same as input file, the "
"input file will be updated only if processing is successful.",
"Existing files will be overwritten (use --no-overwrite to prevent this). "
"If same as input file, the input file will be updated only if "
"processing is successful.",
)
parser.add_argument(
'-l',
@@ -190,6 +191,15 @@ Online documentation is located at:
"may not both use stdout at the same time.",
)
parser.add_argument(
'-n',
'--no-overwrite',
action='store_true',
default=False,
help="If the output file already exists, exit with an error instead of "
"overwriting it.",
)
parser.add_argument(
'--version',
action='version',
+6 -10
View File
@@ -677,12 +677,12 @@ class Fpdf2PdfRenderer:
dy_pdf = -(py_curr_f - py_prev_f)
ops.append(f'{dx_pdf:.2f} {dy_pdf:.2f} Td')
# Determine text to render and compute Tz
# Determine text to render
if not is_last:
next_text, next_x_baseline, _, _ = word_render_data[i + 1]
advance = next_x_baseline - x_baseline
# Add trailing space unless both words are CJK-only
# Add trailing space for text extraction unless both are CJK
if (
advance > 0
and not (
@@ -691,18 +691,14 @@ class Fpdf2PdfRenderer:
)
):
text_to_render = text + ' '
natural_w = pdf.get_string_width(text_to_render)
render_tz = (
(advance / natural_w) * 100
if natural_w > 0
else word_tz
)
else:
text_to_render = text
render_tz = word_tz
else:
text_to_render = text
render_tz = word_tz
# Use word_tz (fits word into its hOCR bbox) — Td handles
# inter-word gaps, so Tz should not stretch to fill them.
render_tz = word_tz
ops.append(f'{render_tz:.2f} Tz')
ops.append(self._encode_shaped_text(pdf, text_to_render))
+2 -2
View File
@@ -681,9 +681,9 @@ def optimize(
safe_symlink(input_file, output_file)
return output_file
if options.jpg_quality == 0:
if not options.jpg_quality:
options.jpg_quality = DEFAULT_JPEG_QUALITY if options.optimize < 3 else 40
if options.png_quality == 0:
if not options.png_quality:
options.png_quality = DEFAULT_PNG_QUALITY if options.optimize < 3 else 30
with Pdf.open(input_file) as pdf:
Generated
+3 -1
View File
@@ -1283,7 +1283,7 @@ wheels = [
[[package]]
name = "ocrmypdf"
version = "17.2.0"
version = "17.3.0"
source = { editable = "." }
dependencies = [
{ name = "deprecation" },
@@ -1313,6 +1313,7 @@ webservice = [
[package.dev-dependencies]
dev = [
{ name = "cyclopts" },
{ name = "ipykernel" },
{ name = "mypy" },
{ name = "reportlab" },
@@ -1367,6 +1368,7 @@ provides-extras = ["watcher", "webservice"]
[package.metadata.requires-dev]
dev = [
{ name = "cyclopts", specifier = ">=4.5.1" },
{ name = "ipykernel", specifier = ">=6.29.5" },
{ name = "mypy", specifier = ">=1.13.0" },
{ name = "reportlab", specifier = ">=4.4.4" },