Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1c2adc3d89 | ||
|
|
37923ffe52 | ||
|
|
773e28478c | ||
|
|
e6db071273 | ||
|
|
de74b80335 | ||
|
|
5cba68b93d | ||
|
|
d2d39de92f | ||
|
|
0b834411fe | ||
|
|
c935ba070b |
+1
-1
@@ -44,7 +44,7 @@ execution. To do this, it will:
|
|||||||
The Python process that calls ``ocrmypdf.ocr()`` must be sufficiently
|
The Python process that calls ``ocrmypdf.ocr()`` must be sufficiently
|
||||||
privileged to perform these actions.
|
privileged to perform these actions.
|
||||||
|
|
||||||
There is no currently no option to manage how jobs are scheduled other
|
There currently is no option to manage how jobs are scheduled other
|
||||||
than the argument ``jobs=`` which will limit the number of worker
|
than the argument ``jobs=`` which will limit the number of worker
|
||||||
processes.
|
processes.
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ PDF is a rich, complex file format. The official PDF 1.7 specification,
|
|||||||
ISO 32000:2008, is hundreds of pages long and references several annexes
|
ISO 32000:2008, is hundreds of pages long and references several annexes
|
||||||
each of which are similar in length. PDFs can contain video, audio, XML,
|
each of which are similar in length. PDFs can contain video, audio, XML,
|
||||||
JavaScript and other programming, and forms. In some cases, they can
|
JavaScript and other programming, and forms. In some cases, they can
|
||||||
open internet connections to pre-selected URLs. All of these possible
|
open internet connections to pre-selected URLs. All of these are possible
|
||||||
attack vectors.
|
attack vectors.
|
||||||
|
|
||||||
In short, PDFs `may contain
|
In short, PDFs `may contain
|
||||||
@@ -31,7 +31,7 @@ describes a high-paranoia method which allows potentially hostile PDFs
|
|||||||
to be viewed and rasterized safely in a disposable virtual machine. A
|
to be viewed and rasterized safely in a disposable virtual machine. A
|
||||||
trusted PDF created in this manner is converted to images and loses all
|
trusted PDF created in this manner is converted to images and loses all
|
||||||
information making it searchable and losing all compression. OCRmyPDF
|
information making it searchable and losing all compression. OCRmyPDF
|
||||||
could be used restore searchability.
|
could be used to restore searchability.
|
||||||
|
|
||||||
How OCRmyPDF processes PDFs
|
How OCRmyPDF processes PDFs
|
||||||
===========================
|
===========================
|
||||||
@@ -66,8 +66,8 @@ service. OCRmyPDF relies on Ghostscript, and therefore, if deployed
|
|||||||
online one should be prepared to comply with Ghostscript's Affero GPL
|
online one should be prepared to comply with Ghostscript's Affero GPL
|
||||||
license, and any other licenses.
|
license, and any other licenses.
|
||||||
|
|
||||||
Setting aside these concerns, a side effect of OCRmyPDF is it may
|
Setting aside these concerns, a side effect of OCRmyPDF is that it may
|
||||||
incidentally sanitize PDFs that contain certain types of malware. It
|
incidentally sanitize PDFs containing certain types of malware. It
|
||||||
repairs the PDF with pikepdf/libqpdf, which could correct malformed PDF
|
repairs the PDF with pikepdf/libqpdf, which could correct malformed PDF
|
||||||
structures that are part of an attack. When PDF/A output is selected
|
structures that are part of an attack. When PDF/A output is selected
|
||||||
(the default), the input PDF is partially reconstructed by Ghostscript.
|
(the default), the input PDF is partially reconstructed by Ghostscript.
|
||||||
@@ -83,7 +83,7 @@ Limiting CPU usage
|
|||||||
OCRmyPDF will attempt to use all available CPUs and storage, so
|
OCRmyPDF will attempt to use all available CPUs and storage, so
|
||||||
executing ``nice ocrmypdf`` or limiting the number of jobs with the
|
executing ``nice ocrmypdf`` or limiting the number of jobs with the
|
||||||
``-j`` argument may ensure the server remains available. Another option
|
``-j`` argument may ensure the server remains available. Another option
|
||||||
would be run OCRmyPDF jobs inside a Docker container, a virtual machine,
|
would be to run OCRmyPDF jobs inside a Docker container, a virtual machine,
|
||||||
or a cloud instance, which can impose its own limits on CPU usage and be
|
or a cloud instance, which can impose its own limits on CPU usage and be
|
||||||
terminated "from orbit" if it fails to complete.
|
terminated "from orbit" if it fails to complete.
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,19 @@ The public API may be useful in scripts that launch OCRmyPDF processes or that
|
|||||||
wish to use some of its features for working with PDFs.
|
wish to use some of its features for working with PDFs.
|
||||||
|
|
||||||
|
|
||||||
|
v12.3.0
|
||||||
|
=======
|
||||||
|
|
||||||
|
- Fixed a regression introduced in Pillow 8.3.0. Pillow no longer rounds DPI
|
||||||
|
for image resolutions. We now account for this. (:issue:`802`)
|
||||||
|
- We no longer use some API calls that are deprecated in the latest versions of
|
||||||
|
pikepdf.
|
||||||
|
- Improved error message when a language is requested that doesn't look like a
|
||||||
|
typical ISO 639-2 code.
|
||||||
|
- Fixed some tests that attempted to symlink on Windows, breaking tests on a
|
||||||
|
Windows desktop but not usually on CI.
|
||||||
|
- Documentation fixes (thanks to @mara004)
|
||||||
|
|
||||||
v12.2.0
|
v12.2.0
|
||||||
=======
|
=======
|
||||||
|
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ test =
|
|||||||
# or brew install exempi
|
# or brew install exempi
|
||||||
docs =
|
docs =
|
||||||
sphinx
|
sphinx
|
||||||
sphinx_rtd_theme
|
sphinx-rtd-theme
|
||||||
sphinx-issues
|
sphinx-issues
|
||||||
extended_test =
|
extended_test =
|
||||||
PyMuPDF == 1.13.4
|
PyMuPDF == 1.13.4
|
||||||
|
|||||||
+10
-2
@@ -47,7 +47,8 @@ def strip_invisible_text(pdf, page):
|
|||||||
render_mode = 0
|
render_mode = 0
|
||||||
text_objects = []
|
text_objects = []
|
||||||
|
|
||||||
page.page_contents_coalesce()
|
rich_page = pikepdf.Page(page)
|
||||||
|
rich_page.contents_coalesce()
|
||||||
for operands, operator in pikepdf.parse_content_stream(page, ''):
|
for operands, operator in pikepdf.parse_content_stream(page, ''):
|
||||||
if not in_text_obj:
|
if not in_text_obj:
|
||||||
if operator == pikepdf.Operator('BT'):
|
if operator == pikepdf.Operator('BT'):
|
||||||
@@ -307,7 +308,14 @@ class OcrGrafter:
|
|||||||
if strip_old_text:
|
if strip_old_text:
|
||||||
strip_invisible_text(self.pdf_base, base_page)
|
strip_invisible_text(self.pdf_base, base_page)
|
||||||
|
|
||||||
base_page.page_contents_add(new_text_layer, prepend=True)
|
if hasattr(pikepdf.Page, 'contents_add'):
|
||||||
|
# pikepdf >= 2.14 adds this method and deprecates the one below
|
||||||
|
pikepdf.Page(base_page).contents_add(new_text_layer, prepend=True)
|
||||||
|
else:
|
||||||
|
# pikepdf < 2.14
|
||||||
|
base_page.page_contents_add(
|
||||||
|
new_text_layer, prepend=True
|
||||||
|
) # pragma: no cover
|
||||||
|
|
||||||
_update_resources(
|
_update_resources(
|
||||||
obj=base_page, font=font, font_key=font_key, procset=procset
|
obj=base_page, font=font, font_key=font_key, procset=procset
|
||||||
|
|||||||
@@ -65,13 +65,14 @@ def check_options_languages(options, ocr_engine_languages):
|
|||||||
log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE)
|
log.debug("No language specified; assuming --language %s", DEFAULT_LANGUAGE)
|
||||||
if not ocr_engine_languages:
|
if not ocr_engine_languages:
|
||||||
return
|
return
|
||||||
if not options.languages.issubset(ocr_engine_languages):
|
missing_languages = options.languages - ocr_engine_languages
|
||||||
|
if missing_languages:
|
||||||
msg = (
|
msg = (
|
||||||
f"OCR engine does not have language data for the following "
|
f"OCR engine does not have language data for the following "
|
||||||
"requested languages: \n"
|
"requested languages: \n"
|
||||||
)
|
)
|
||||||
for lang in options.languages - ocr_engine_languages:
|
msg += '\n'.join(lang for lang in missing_languages)
|
||||||
msg += lang + '\n'
|
msg += '\nNote: most languages are identified by a 3-digit ISO 639-2 Code'
|
||||||
raise MissingDependencyError(msg)
|
raise MissingDependencyError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+21
-2
@@ -25,19 +25,31 @@ log = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class Resolution(namedtuple('Resolution', ('x', 'y'))):
|
class Resolution(namedtuple('Resolution', ('x', 'y'))):
|
||||||
"""The number of pixels per inch in each 2D direction."""
|
"""The number of pixels per inch in each 2D direction.
|
||||||
|
|
||||||
|
Resolution objects are considered "equal" for == purposes if they are
|
||||||
|
equal to a reasonable tolerance.
|
||||||
|
"""
|
||||||
|
|
||||||
__slots__ = ()
|
__slots__ = ()
|
||||||
|
|
||||||
|
# rel_tol after converting from dpi to pixels per meter and saving
|
||||||
|
# as integer with rounding, as many file formats
|
||||||
|
CONVERSION_ERROR = 0.002
|
||||||
|
|
||||||
def round(self, ndigits: int):
|
def round(self, ndigits: int):
|
||||||
return Resolution(round(self.x, ndigits), round(self.y, ndigits))
|
return Resolution(round(self.x, ndigits), round(self.y, ndigits))
|
||||||
|
|
||||||
def to_int(self):
|
def to_int(self):
|
||||||
return Resolution(int(round(self.x)), int(round(self.y)))
|
return Resolution(int(round(self.x)), int(round(self.y)))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _isclose(cls, a, b):
|
||||||
|
return isclose(a, b, rel_tol=cls.CONVERSION_ERROR)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_square(self) -> bool:
|
def is_square(self) -> bool:
|
||||||
return isclose(self.x, self.y, rel_tol=1e-3)
|
return self._isclose(self.x, self.y)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_finite(self) -> bool:
|
def is_finite(self) -> bool:
|
||||||
@@ -61,6 +73,13 @@ class Resolution(namedtuple('Resolution', ('x', 'y'))):
|
|||||||
def __repr__(self): # pragma: no cover
|
def __repr__(self): # pragma: no cover
|
||||||
return f"Resolution({self.x}x{self.y} dpi)"
|
return f"Resolution({self.x}x{self.y} dpi)"
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if isinstance(other, tuple) and len(other) == 2:
|
||||||
|
other = Resolution(*other)
|
||||||
|
if not isinstance(other, Resolution):
|
||||||
|
return NotImplemented
|
||||||
|
return self._isclose(self.x, other.x) and self._isclose(self.y, other.y)
|
||||||
|
|
||||||
|
|
||||||
class NeverRaise(Exception):
|
class NeverRaise(Exception):
|
||||||
"""An exception that is never raised"""
|
"""An exception that is never raised"""
|
||||||
|
|||||||
@@ -72,7 +72,7 @@ def test_rasterize_rotated(francais, outdir, caplog):
|
|||||||
|
|
||||||
with Image.open(outdir / 'out.png') as im:
|
with Image.open(outdir / 'out.png') as im:
|
||||||
assert im.size == (target_size[1], target_size[0])
|
assert im.size == (target_size[1], target_size[0])
|
||||||
assert im.info['dpi'] == (forced_dpi[1], forced_dpi[0])
|
assert im.info['dpi'] == forced_dpi.flip_axis()
|
||||||
|
|
||||||
|
|
||||||
def test_gs_render_failure(resources, outpdf):
|
def test_gs_render_failure(resources, outpdf):
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ from ocrmypdf import helpers
|
|||||||
|
|
||||||
from .conftest import running_in_docker
|
from .conftest import running_in_docker
|
||||||
|
|
||||||
|
needs_symlink = pytest.mark.skipif(os.name == 'nt', reason='needs posix symlink')
|
||||||
|
|
||||||
|
|
||||||
class TestSafeSymlink:
|
class TestSafeSymlink:
|
||||||
def test_safe_symlink_link_self(self, tmp_path, caplog):
|
def test_safe_symlink_link_self(self, tmp_path, caplog):
|
||||||
@@ -27,6 +29,7 @@ class TestSafeSymlink:
|
|||||||
with pytest.raises(FileExistsError):
|
with pytest.raises(FileExistsError):
|
||||||
helpers.safe_symlink(tmp_path / 'input', tmp_path / 'regular_file')
|
helpers.safe_symlink(tmp_path / 'input', tmp_path / 'regular_file')
|
||||||
|
|
||||||
|
@needs_symlink
|
||||||
def test_safe_symlink_relink(self, tmp_path):
|
def test_safe_symlink_relink(self, tmp_path):
|
||||||
(tmp_path / 'regular_file_a').touch()
|
(tmp_path / 'regular_file_a').touch()
|
||||||
(tmp_path / 'regular_file_b').write_bytes(b'ABC')
|
(tmp_path / 'regular_file_b').write_bytes(b'ABC')
|
||||||
@@ -77,6 +80,7 @@ class TestFileIsWritable:
|
|||||||
def test_plain(self, non_existent):
|
def test_plain(self, non_existent):
|
||||||
assert helpers.is_file_writable(non_existent)
|
assert helpers.is_file_writable(non_existent)
|
||||||
|
|
||||||
|
@needs_symlink
|
||||||
def test_symlink_loop(self, tmp_path):
|
def test_symlink_loop(self, tmp_path):
|
||||||
loop = tmp_path / 'loop'
|
loop = tmp_path / 'loop'
|
||||||
loop.symlink_to(loop)
|
loop.symlink_to(loop)
|
||||||
|
|||||||
Reference in New Issue
Block a user