Compare commits

...
157 Commits
Author SHA1 Message Date
James R. Barlow cb2f090c60 v16.3.1 release notes 2024-05-21 01:39:30 -07:00
James R. Barlow f3d6387bca Fix "OCR" progress bar not matching actual progress 2024-05-21 01:35:14 -07:00
James R. Barlow abf9729c61 Semfree test: accept pdfa conversion failed as a valid return code
Fixes #1316
2024-05-21 01:26:11 -07:00
James R. Barlow 442e9c9f0d Add missing codecov token where missed & drop unneeded brew openssl 2024-05-19 01:07:38 -07:00
James R. Barlow 397fad249d v16.3.0 release notes 2024-05-19 00:50:59 -07:00
James R. Barlow 9a3c5a3f7c Add progressbar for metadata_fixup
Might take time for big files. Pdf.open() potentially is expensive as well, but QPDF doesn't give us progress feedback for that.

Closes Show progress during postprocessing #1313
2024-05-19 00:46:50 -07:00
James R. Barlow 950c700274 Fix Ghostscript PDF/A progressbar not displaying 2024-05-19 00:44:21 -07:00
James R. Barlow 26432c38a9 Raise exception if rotate pages threshold adjusted without --rotate-pages
Fixes Make usage of --rotate-pages-threshold clearer #1309
2024-05-18 23:49:27 -07:00
James R. Barlow 28be50136c hocr: If a line box's coords are invalid, log and error and don't render
Addresses [Bug]: Crash on multiple .pdf files #1312

Not actually a fix, but at least it will get us better diagnostics. Appears old Tesseract 4.x generates bad line boxes at times.
2024-05-18 23:32:18 -07:00
James R. Barlow 0c62f2de5d Issue template: check for EOL OS 2024-05-17 19:51:15 -07:00
James R. Barlow 5caf654f22 Add new codecov token 2024-05-11 01:03:41 -07:00
James R. Barlow 205593445e Change test to run on macos x64 and arm64 2024-05-11 00:13:08 -07:00
James R. Barlow f25fb8c63a Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-05-08 00:39:27 -07:00
James R. Barlow 99c78650b6 Add better error message for PDFs with invalid CTMs
Closes #1303
2024-05-07 14:00:30 -07:00
Ahmed AbdouandGitHub 08e89e2dbe Adding language install docs for archlinux (#1296)
Adding language install docs for archlinux
2024-04-24 14:46:05 -07:00
James R. Barlow 0e013df161 v16.2.0 release notes 2024-04-16 00:37:03 -07:00
James R. Barlow 9ba4e3ab46 Log unusual exceptions when trying to obtain a version
Fixes #1262
2024-04-07 14:39:08 -07:00
James R. Barlow 5fdcb7602b Make downsampling large images that Tesseract would otherwise error on into default behavior
Fixes #1281
2024-04-07 13:43:20 -07:00
James R. Barlow b4db1b741f optimize: fix handling of [/FlateDecode none] - type images
Closes #1271
2024-04-07 01:44:08 -07:00
James R. Barlow 7a8cc21e31 Add support for sidecar output to io.BytesIO
Closes #1252
2024-04-07 01:38:55 -07:00
James R. Barlow 0674829d8f Remove tool.black config 2024-04-07 00:36:52 -07:00
James R. Barlow 315aa0474b Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-04-07 00:34:51 -07:00
Ben BeasleyandGitHub df3451e779 Update the typer[all] dependency to typer-slim[standard] (#1287)
In 0.12.1, Typer was significantly reorganized.

- `typer-slim` is the library (for `import typer`)
- `typer-slim[standard]` adds optional dependencies (currently `rich`
  and `shellingham`, basically equivalent to the old `typer[all]`)
- `typer-cli` is the `typer` command-line tool
- `typer` is now basically a metapackage that brings in *all of the
  above*, and it no longer has an `all` extra

Pip will warn about this and proceed,

```
WARNING: typer 0.12.1 does not provide the extra 'all'
```

but there are other tools that will fail hard when asked to resolve a
(now) nonexistent extra.

Since this project doesn’t need the `typer` command-line tool, it looks
like changing the dependency to `typer-slim[standard]` is the best way
forward.

See https://typer.tiangolo.com/release-notes/#0121 and
tiangolo/typer#785 for further discussion
and details.
2024-04-07 00:34:34 -07:00
akierigandGitHub 3ba42802d1 added Macports install information (#1286) 2024-04-07 00:33:57 -07:00
James R. Barlow d6342cb8c2 Add heif/heic input image support 2024-04-07 00:33:13 -07:00
James R. Barlow 065bddbc6c Reformat with ruff format 2024-04-07 00:25:32 -07:00
James R. Barlow 067f429dde Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-03-26 15:34:00 -07:00
Daniel LovegroveandGitHub 6895c2d70f Fix Broken Documentation Links (#1275)
* Update URL for PDFMARK documentation

For reference, here is a link to the old PDF:
https://web.archive.org/web/20190806035303/https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf

It appears Adobe converted the PDF into a webpage-based document, the
wording seems to almost identical b/w the PDF and the website.

* Fix cross-references to JBIG2 page

* Fix links for Fedora + Arch + HEAD revision install

Fedora 39 has been released, and the package tracker no longer includes
a release overview for Fedora 37 hence why it was removed here.
2024-03-22 14:38:52 -07:00
James R. Barlow 686481982a Fix naming of hOCR rendered files 2024-03-22 13:27:20 -07:00
James R. Barlow a9e1d19b78 v16.1.2 release notes 2024-03-20 12:56:13 -07:00
James R. Barlow f95aa63718 Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-03-20 12:26:02 -07:00
James Barlow 855de287b2 Fix test suite failure with Ghostscript >= 10.3
Ghostscript is more picky about a specific case with SMask that cannot be converted to PDF/A

Details here
https://github.com/ArtifexSoftware/ghostpdl/commit/4dcfae36bb4dcbc4ef3b5e5afc98bcde0d6b9ddc
2024-03-19 17:20:33 -07:00
NilsRoandGitHub feeb9f213f batch example: added archive, small corrections and optimizations (#1277)
* Added archive, small corrections

Added a function to archive originals and avoid calling ocrmypdf if they are still is PDF/A.

* Added Copyright
2024-03-18 13:22:24 -07:00
Emiel MolenaarandGitHub e7eb8fa805 Update Dockerfile.alpine (#1268)
Use Alpine 3.19 as base image to ensure we get GhostScript 10.2.1 to eliminate serious regressions that corrupt PDFs with existing text.
2024-03-13 14:49:42 -07:00
James R. Barlow 8a747f005a pixels -> megapixels
Fixes #1265
2024-02-29 15:31:07 -08:00
James R. Barlow 16ab4a8b4e Fix error message about missing Python exec
Message is
unable to start container process: exec: "python": executable file not found in $PATH: unknown.

Closes #1260
2024-02-21 23:54:41 -08:00
James R. Barlow 8d30cff4ef Undo future annotations from watcher.py till Typer fixes its issue
Fixes #1258
2024-02-20 19:14:39 -08:00
James R. Barlow 59d5b0d1bd v16.1.1 release notes 2024-02-15 16:56:25 -08:00
James R. Barlow 9ec0745ab8 Try pypy3.10 2024-02-14 14:25:13 -08:00
James R. Barlow 3a3635f7f9 Python 3.10 cleanup, manual fixes 2024-02-14 12:48:17 -08:00
James R. Barlow 6a746a1cbb ruff linting/Python 3.10 cleanup 2024-02-14 12:41:51 -08:00
James R. Barlow 906c130f96 Update rust toml settings 2024-02-14 12:32:26 -08:00
James R. Barlow 4a78458821 v16.1.0 release notes 2024-02-12 01:46:21 -08:00
James R. Barlow fddf3ce2f4 Clarify warnings filter 2024-02-12 01:43:47 -08:00
James R. Barlow 353b34e695 Merge branch 'feature/pageboxes' 2024-02-12 01:41:56 -08:00
James R. Barlow 7d63355c3c Use hocr renderer for LTR languages 2024-02-12 01:41:41 -08:00
James R. Barlow 42ff7fc842 Fix handling of pages that are restored to correct orientation with /Rotate
Appears inversion of CTM was incorrect, introduced in commit 9898904
2024-02-12 01:32:26 -08:00
James R. Barlow 26470fe16a Suppress reportlab deprecation warning 2024-02-12 01:17:08 -08:00
James R. Barlow 3b9d4b7f0a Attempt to deal with oddball mediaboxes 2024-02-11 15:34:54 -08:00
James R. Barlow 11f53fe9a9 First cut at propagating page boxes
This would fix the immediate issue, but does not address an offset mediabox.
2024-02-11 15:34:54 -08:00
James R. Barlow 123c0c766f Mention pipx, install --user --upgrade
Closes #1249
2024-02-08 09:42:00 -08:00
James R. Barlow 6a9be2142e Advise Homebrew on Linux for Ubuntu 20.04 2024-02-07 19:52:50 -08:00
James R. Barlow 0bc350f55e Merge branch 'main' of github.com:ocrmypdf/OCRmyPDF 2024-02-06 01:28:10 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
7a6edf62ba Bump codecov/codecov-action from 3 to 4 (#1247)
Bumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.
- [Release notes](https://github.com/codecov/codecov-action/releases)
- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: codecov/codecov-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-05 03:55:13 -08:00
James R. Barlow 07b6f06f11 optimize: log images with unclear decode tables 2024-02-01 15:42:40 -08:00
nisbet-hubbardandGitHub 2005f622bb Update gs dependency & instructions for RHEL (#1228)
* Update gs depedency to 9.54

* Add instructions for RHEL

* Update instruction for using python3.11 on RHEL
2024-01-24 10:07:58 -08:00
James R. Barlow cca04fd799 Document ability to remove all OCR 2024-01-23 23:35:43 -08:00
MaxiandGitHub 75bf8e4ba2 Add autotools automake libtool and leptonica requirements (#1230)
These tools are required for a successful install of JBIG2 on Ubuntu
2024-01-09 11:58:17 -08:00
Anish SapkotaandGitHub daabb5b100 Minor english correction in Docs (#1229) 2024-01-08 10:18:26 -08:00
James R. Barlow 035ebea72f Change Windows build to use older Ghostscript 2024-01-07 01:56:38 -08:00
James R. Barlow a499956462 v16.0.4 release notes 2024-01-07 01:39:06 -08:00
James R. Barlow 74d2a156c4 Update cache 2024-01-07 01:35:05 -08:00
James R. Barlow f87fc7b12d gs: check for text-corrupting versions 2024-01-07 01:33:49 -08:00
James R. Barlow 602f5632cb hocr: fix unbound local 2024-01-07 01:33:36 -08:00
James R. Barlow 9fbbcf7599 Merge branch 'feature/hocrdebug' 2024-01-07 01:22:54 -08:00
James R. Barlow 9498f01f59 hocr: Pass text direction for standalone words 2024-01-06 16:21:21 -08:00
James R. Barlow 2c59aca5a1 Fix render_mode ON_TOP when previous content stream does not restore CTM 2024-01-06 16:20:55 -08:00
James R. Barlow 51301d69c9 Change default graft render mode back to underneath
It was mistakenly changed to on top at some point
2024-01-06 16:20:30 -08:00
James R. Barlow 7e608fd1df Use Courier as debug font 2024-01-06 14:24:25 -08:00
James R. Barlow ecc79315df Adjust issue template files description 2024-01-04 00:27:06 -08:00
James R. Barlow 14365d10b8 Skip testing oom killer on Python 3.12
Need to investigate further if there's a safe way to do this test.
2024-01-02 16:28:22 -08:00
James R. Barlow 5e5320020f v16.0.3 release notes 2024-01-02 02:49:54 -08:00
James R. Barlow 103c3e0cd6 Drop Ghostscript CVE warning and adjust version down to 9.54
Most distros have probably now backported the CVE fix. Dropped version to 9.54 so RHEL 9 can use recent ocrmypdf.
2024-01-01 13:02:57 -08:00
Anthony NabilandGitHub 7a1c89edd9 fixed a spelling mistake (#1222) 2023-12-30 15:53:22 -08:00
James R. Barlow a5ff3d2f42 Update instructions about custom language packages
Closes #1218
2023-12-27 16:46:10 -08:00
James R. Barlow b71d16dd96 Draw RTL text with reversed matrix instead of reversing characters 2023-12-24 01:34:03 -08:00
James R. Barlow fd593eb5e9 Reversing character order for RTL helps output 2023-12-24 01:24:44 -08:00
James R. Barlow a0b98abb94 Improve and externalize debug rendering settings 2023-12-24 00:10:09 -08:00
James R. Barlow 18353e1e94 Also fix space rendering 2023-12-24 00:09:47 -08:00
James R. Barlow 9adcad84da Overhaul debug rendering to support possible use of Helvetica for testing 2023-12-23 23:47:35 -08:00
James R. Barlow f2714586d8 Fix upside down glyphless font 2023-12-23 23:44:37 -08:00
James R. Barlow 0b6fb62967 Update issue template 2023-12-23 16:21:17 -08:00
James R. Barlow 1db8b0b943 Temporarily revert default renderer to sandwich while investigating regressions in macOS Preview 2023-12-23 16:19:44 -08:00
James R. Barlow f38aebb3d5 Update issue template 2023-12-23 15:46:43 -08:00
James R. Barlow 7162c36d37 Name artifacts explicitly 2023-12-20 13:53:31 -08:00
James R. Barlow f4d4ea46c8 Update artifact actions 2023-12-20 12:44:43 -08:00
James R. Barlow 2fd1a0f178 v16.0.1 release notes 2023-12-20 12:33:41 -08:00
James R. Barlow 73ed33a086 Tighten dependencies 2023-12-20 12:33:18 -08:00
James R. Barlow e6095a9949 Fix text rendering issue with new hOCR text renderer 2023-12-20 12:26:06 -08:00
James R. Barlow 16f05af401 Fix release notes - drop rc from version 2023-12-18 20:08:45 -08:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1631afc878 Bump actions/setup-python from 4 to 5 (#1205)
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 4 to 5.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-17 15:35:13 -08:00
Robin RichtsfeldandGitHub 63d87fc440 Fix --fast-web-view documentation (#1206) 2023-12-17 14:54:38 -08:00
James R. Barlow 9489c01259 Skip test_encrypted on Py3.12 + macOS 2023-12-08 00:12:24 -08:00
James R. Barlow 30d92ad83f Fix build settings to adjust for dropping py39 2023-12-07 23:40:45 -08:00
James R. Barlow a4987733c4 Filter rl_safe_eval deprecation warning
Full message
eportlab/lib/rl_safe_eval.py:11: DeprecationWarning: ast.NameConstant is deprecated and will be removed in Python 3.14; use ast.Constant instead
    haveNameConstant = hasattr(ast,'NameConstant')

Warning triggered by reportlab-4.0.7 and Python 3.12
2023-12-07 23:40:23 -08:00
James R. Barlow 39eee05230 v16.0.0rc1 release notes
Fixes #1009, #1191, #1157
2023-12-03 15:44:34 -08:00
James R. Barlow 5b2f2e6290 Merge branch 'feature/modernhocr' 2023-12-03 15:17:02 -08:00
James R. Barlow 445617a1a5 Rebuild cache for hocr default case 2023-12-03 15:16:18 -08:00
James R. Barlow f6e90a5934 hOCR renderer is now default 2023-12-02 19:58:00 -08:00
James R. Barlow 43618e6b3f Move canvas API to pikepdf and import it 2023-12-02 19:42:35 -08:00
James R. Barlow e97f89de3b Refactor font so glyphless isn't as hard coded 2023-12-02 08:55:01 -08:00
James R. Barlow 11d3e32f1e Fix hocrtransform CLI 2023-12-02 08:08:29 -08:00
James R. Barlow 2affa83efe Remove code that attempted to manage xattrs out of output file
Feature requested in issue #1179, but caused #1195. On further review,
there is no platform independent way to manage extended attributes
and it is not clear copying them through is necessarily the sensible
thing to do.

Closes #1179.
2023-11-29 23:25:51 -08:00
James R. Barlow c90d5cd84b Fix Ghostscript installation instructions and add warning 2023-11-29 14:10:04 -08:00
James R. Barlow aacaba3d26 Ignore pypy for now 2023-11-21 01:05:23 -08:00
James R. Barlow fec53be841 Remove next major release deprecations 2023-11-21 00:47:51 -08:00
James R. Barlow 3f7b540f76 Drop Python 3.9 support 2023-11-21 00:46:00 -08:00
James R. Barlow d217856166 Make hocrdebug work, and try to handle CJK spacing better 2023-11-21 00:33:02 -08:00
James R. Barlow e2be457e9b Avoid divzero 2023-11-20 23:08:00 -08:00
James R. Barlow 4850f486d2 Make text API more like an accessor 2023-11-20 22:59:50 -08:00
James R. Barlow 729c7febd9 Fix placement of spaces in debug mode 2023-11-20 22:44:12 -08:00
James R. Barlow 6c6aca2f1e Refactor save_state 2023-11-20 22:29:21 -08:00
James R. Barlow c69823f496 Refactor; accumulate content stream as bytes rather than discrete pikepdf objects 2023-11-20 22:11:59 -08:00
James R. Barlow 73f8f6aac8 Add RTL output - seems to work, but debug does not 2023-11-20 20:28:07 -08:00
James R. Barlow d944254e45 hocr: typing cont'd 2023-11-20 17:07:52 -08:00
James R. Barlow f7ddffe554 hocr: typing 2023-11-20 16:52:55 -08:00
James R. Barlow 8a73ed5d5a Fix JBIG2 not updating progress bar 2023-11-20 16:25:30 -08:00
James R. Barlow 03669183d7 Rationalize canvas interface 2023-11-20 15:54:13 -08:00
James R. Barlow 74e101a2fa Improve canvas interface with chaining 2023-11-20 14:42:48 -08:00
James R. Barlow 532cf18ad3 Restructure hocrtransform submodule to avoid having everything in __init__ 2023-11-20 00:57:58 -08:00
James R. Barlow 0b90b697e2 More tidying 2023-11-20 00:43:43 -08:00
James R. Barlow 6be7c5f7c8 Fix colors and space box rendering 2023-11-20 00:30:54 -08:00
James R. Barlow db2e5132e6 Remove some obsolete parameters 2023-11-20 00:10:55 -08:00
James R. Barlow b14f6f778a Tidying new hOCR renderer 2023-11-19 23:51:27 -08:00
James R. Barlow 415de77457 imageops: fix annots since not using singledispatch anymore 2023-11-19 23:51:27 -08:00
James R. Barlow a9466c4f58 Improve word box positioning 2023-11-19 23:51:27 -08:00
James R. Barlow d9ae453a63 Significantly improvement overall 2023-11-19 23:51:27 -08:00
James R. Barlow 9841e09233 More adjustments 2023-11-19 23:51:27 -08:00
James R. Barlow 0ca314e066 Replace Rect with pikepdf.Rectangle, migrate line matrix to page 2023-11-19 23:51:27 -08:00
James R. Barlow d7680cae27 Correcting Matrix logic helps
The good: don't have to do inverse and intermediate transforms.

The bad: skew looks bad, partly because the hOCR coordinate system is inconsistent around skew?
2023-11-19 23:51:27 -08:00
James R. Barlow 491b6bdb1f Remove concept of HOCR_OK_LANGS 2023-11-19 23:51:27 -08:00
James R. Barlow c591f9601a Remove Latin hOCR test 2023-11-19 23:51:27 -08:00
James R. Barlow 8d1e75017e Remote reportlab backend and make reportlab a test-only dependency 2023-11-19 23:51:27 -08:00
James R. Barlow 94615f7ad4 hOCR now works for all languages 2023-11-19 23:51:27 -08:00
James R. Barlow e5df8e1315 Nearly pixel perfect 2023-11-19 23:51:27 -08:00
James R. Barlow d739b91aef Tidy up 2023-11-19 23:51:27 -08:00
James R. Barlow 686cfb2539 Add rendering of space between boxes 2023-11-19 23:51:27 -08:00
James R. Barlow 2633716bb7 Render interword spaces separately and avoid box overlap 2023-11-19 23:51:27 -08:00
James R. Barlow 0a07c0a44e Fix more things 2023-11-19 23:51:27 -08:00
James R. Barlow 2ca6e110ca Fix private accessors, rename pdf to canvas 2023-11-19 23:51:27 -08:00
James R. Barlow 334a07c839 Refactor debug printing 2023-11-19 23:51:27 -08:00
James R. Barlow a57c39358d Refactor: extract methods 2023-11-19 23:51:27 -08:00
James R. Barlow 30a0c315fb Further exploratory improvements 2023-11-19 23:51:27 -08:00
James R. Barlow b860f0d94c Make coordinate system more consistent 2023-11-19 23:51:27 -08:00
James R. Barlow 14f4c19f5a WIP improve text positioning (not there yet) 2023-11-19 23:51:27 -08:00
James R. Barlow 7ab5c55d46 More colors 2023-11-19 23:51:27 -08:00
James R. Barlow 8b6ecd5971 Fix line and rect drawing 2023-11-19 23:51:27 -08:00
James R. Barlow 7b0871ae4c Fix position errors; ignore non-glyphless font 2023-11-19 23:51:27 -08:00
James R. Barlow b73af7ce10 Fix dashes 2023-11-19 23:51:26 -08:00
James R. Barlow 60645717e2 Test pikepdf canvas - renders... something at this point 2023-11-19 23:51:26 -08:00
James R. Barlow 1cbf578538 Fix import of pdf.ttf 2023-11-19 23:51:26 -08:00
James R. Barlow e966c1fceb Add pdf.ttf 2023-11-19 23:51:26 -08:00
James R. Barlow d0133f8641 Create pikepdf backend renderer 2023-11-19 23:51:26 -08:00
James R. Barlow 6d30b497dc Refactor reportlab into generic backend 2023-11-19 23:51:26 -08:00
James R. Barlow f3b89e66eb Refactor reportlab backend out of hocrtransform 2023-11-19 23:51:26 -08:00
James R. Barlow 04154e207c hocrtransform: move to module 2023-11-19 23:51:26 -08:00
James R. Barlow 9898904be7 Fix pikepdf PdfMatrix deprecation warning; v15.4.3 release notes 2023-11-15 20:27:16 -08:00
174 changed files with 5544 additions and 2704 deletions
+1
View File
@@ -64,6 +64,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
img2pdf \
libsm6 libxext6 libxrender-dev \
pngquant \
python-is-python3 \
tesseract-ocr \
tesseract-ocr-chi-sim \
tesseract-ocr-deu \
+1 -1
View File
@@ -1,7 +1,7 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
FROM alpine:3.18 as base
FROM alpine:3.19 as base
ENV LANG=C.UTF-8
ENV TZ=UTC
@@ -1,4 +1,4 @@
name: General issues
name: Installation, packaging, dependencies
description: Installation, packages, dependencies, "nothing works", test suite failures...
title: "[Bug]: "
labels: ["bug", "triage"]
@@ -9,6 +9,10 @@ body:
attributes:
value: |
Thanks for taking the time to fill out this bug report!
If your issue involves using OCRmyPDF on specific file(s) and not getting
good results, this is the *wrong* issue template. Please use the recommended
template to ensure we have enough information to help.
- type: textarea
id: what-happened
attributes:
@@ -20,7 +24,7 @@ body:
- type: dropdown
id: packaging-system
attributes:
label: Where are you installing from?
label: Where are you installing/running from?
multiple: true
options:
- PyPI (pip, poetry, pipx, etc.)
@@ -33,6 +37,11 @@ body:
- source build
validations:
required: true
- type: input
id: version
attributes:
label: OCRmyPDF version
description: Paste "ocrmypdf --version" here
- type: dropdown
id: operating-system
attributes:
@@ -43,6 +52,17 @@ body:
- Windows
- macOS
- BSD
- type: input
id: os_version
attributes:
label: Operating system details and version
- type: checkboxes
attributes:
label: Simple sanity checks
description: Select all that apply
options:
- label: Operating system is currently supported by its vendor (not end of life)
- label: Python version is compatible with OCRmyPDF
- type: textarea
id: logs
attributes:
@@ -21,7 +21,7 @@ body:
id: reproduce
attributes:
label: Steps to reproduce
description: Please include steps to reproduce
description: Please include steps to reproduce.
value: |
1. Run ocrmypdf -v1 ...arguments... input.pdf output.pdf
2. Open output.pdf
@@ -31,8 +31,21 @@ body:
id: files
attributes:
label: Files
description: Please attach the input and output files, or any screenshots that may be helpful.
placeholder: Drag and drop files here
description: |
Please attach the input and output files, or any screenshots that may be helpful.
If you cannot provide a test file, we probably won't be able to help with the issue.
PDF is a complex file format, and there may be technical details in the PDF that are
causing the issue. There's really no substitute for a test file.
We understand files may contain personal or sensitive information. Here are some options:
- Try reproducing the issue with a file from the test suite. (See tests/resources)
- Try to create another file in the same way as your private file.
- Encrypt the file to OCRmyPDF's private GPG key, and then zip the GPG file.
- Use ``qpdf --json yourfile.pdf`` to produce a JSON representation of your file that
omits personal information.
placeholder: |
Drag and drop files here.
- type: dropdown
id: packaging-system
attributes:
+24 -19
View File
@@ -22,20 +22,18 @@ jobs:
strategy:
matrix:
include:
- os: ubuntu-22.04
python: "3.9"
- os: ubuntu-22.04
python: "3.10"
- os: ubuntu-22.04
python: "3.11"
- os: ubuntu-22.04
python: "3.9"
python: "3.10"
tesseract5: true
- os: ubuntu-latest
python: "3.12"
tesseract5: true
#- os: ubuntu-latest
# python: "pypy3.9"
- os: ubuntu-latest
python: "pypy3.10"
env:
OS: ${{ matrix.os }}
@@ -46,7 +44,7 @@ jobs:
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
name: Setup Python
with:
python-version: ${{ matrix.python }}
@@ -102,17 +100,20 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
test_macos:
name: Test macOS
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
os: [macos-latest, macos-13] # macos-latest is arm64, macos-13 is x86_64
python: ["3.10", "3.11", "3.12"]
env:
@@ -133,11 +134,10 @@ jobs:
ghostscript \
jbig2enc \
openjpeg \
openssl \
pngquant \
tesseract
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
name: Setup Python
with:
python-version: ${{ matrix.python }}
@@ -160,7 +160,9 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -182,7 +184,7 @@ jobs:
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
name: Setup Python
with:
python-version: ${{ matrix.python }}
@@ -191,7 +193,7 @@ jobs:
- name: Install system packages
run: |
choco install --yes --no-progress --pre tesseract
choco install --yes --no-progress --ignore-checksums ghostscript
choco install --yes --no-progress --ignore-checksums ghostscript --version 9.56.1
- name: Install Python packages
run: |
@@ -203,7 +205,9 @@ jobs:
python -m pytest --cov-report xml --cov=ocrmypdf --cov=tests/ -n0 tests/
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v3
uses: codecov/codecov-action@v4
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: ./coverage.xml
env_vars: OS,PYTHON
@@ -216,10 +220,10 @@ jobs:
with:
fetch-depth: "0" # 0=all, needed for setuptools-scm to resolve version tags
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
name: Setup Python
with:
python-version: "3.9"
python-version: "3.10"
cache: "pip"
- name: Make wheels and sdist
@@ -227,8 +231,9 @@ jobs:
python -m pip install --upgrade pip wheel build
python -m build --sdist --wheel
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: artifact
path: |
./dist/*.whl
./dist/*.tar.gz
@@ -242,7 +247,7 @@ jobs:
id-token: write # mandatory for PyPI publishing
if: github.event_name == 'push' && startsWith(github.event.ref, 'refs/tags/v')
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: artifact
path: dist
@@ -259,7 +264,7 @@ jobs:
# Required to create a release
contents: write
steps:
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
with:
name: artifact
path: dist
+1 -1
View File
@@ -19,7 +19,7 @@ formats:
build:
os: ubuntu-22.04
tools:
python: "3.9"
python: "3.10"
python:
install:
+8
View File
@@ -124,6 +124,14 @@ Copyright: Kai-Uwe Behrmann <www.behrmann.name>
ColorSolutions <www.basICColor.com>
License: Zlib
Files: src/ocrmypdf/data/pdf.ttf
Copyright: (C) 2014 Ray Smith
(C) 2015 Ken Sharp
(C) 2016 James R. Barlow
(C) 2016 Jeff Breidenbach
(C) 2017 Zdenko Podobný
License: Apache-2.0
Files: tests/resources/3small.pdf
Copyright: (C) 2014 Euskaldunaa
(C) 2017 James R. Barlow
+1
View File
@@ -70,6 +70,7 @@ Linux, Windows, macOS and FreeBSD are supported. Docker images are also availabl
| Windows Subsystem for Linux | ``apt install ocrmypdf`` |
| Fedora | ``dnf install ocrmypdf`` |
| macOS (Homebrew) | ``brew install ocrmypdf`` |
| macOS (MacPorts) | ``port install ocrmypdf`` |
| macOS (nix) | ``nix-env -i ocrmypdf`` |
| LinuxBrew | ``brew install ocrmypdf`` |
| FreeBSD | ``pkg install py-ocrmypdf`` |
+40 -39
View File
@@ -118,15 +118,16 @@ OCR for huge images
-------------------
Tesseract has internal limits on the size
of images it will process. If you issue
``--tesseract-downsample-large-images``, OCRmyPDF will downsample images
to fit Tesseract limits. (The limits are usually entered only for scanned
images of oversized media, such as large maps or blueprints exceeding
110 cm or 43 inches in either dimension, and at high DPI.)
of images it will process. By default,
``--tesseract-downsample-large-images`` is enabled, and OCRmyPDF will
downsample images to fit Tesseract limits. (The limits are usually encountered
only for scanned images of oversized media, such as large maps or blueprints exceeding
110 cm or 43 inches in either dimension, and at high DPI.) This feature can disabled
using ``--no-tesseract-downsample-large-images``.
``--tesseract-downsample-above Npixels`` adjusts the threshold at which images
will be downsampled. By default, only images that exceed any of Tesseract's
internal limits are downsampled.
internal limits are downsampled (32767 pixels on either dimension).
You will also need to set ``--tesseract-timeout`` high enough to allow
for processing.
@@ -239,46 +240,46 @@ rendering
OCRmyPDF has these PDF renderers: ``sandwich`` and ``hocr``. The
renderer may be selected using ``--pdf-renderer``. The default is
``auto`` which lets OCRmyPDF select the renderer to use. Currently,
``auto`` always selects ``sandwich``.
The ``sandwich`` renderer
-------------------------
The ``sandwich`` renderer uses Tesseract's new text-only PDF feature,
which produces a PDF page that lays out the OCR in invisible text. This
page is then "sandwiched" onto the original PDF page, allowing lossless
application of OCR even to PDF pages that contain other vector objects.
Currently this is the best renderer for most uses, however it is
implemented in Tesseract so OCRmyPDF cannot influence it. Currently some
problematic PDF viewers like Mozilla PDF.js and macOS Preview have
problems with segmenting its text output, and
mightrunseveralwordstogether.
When image preprocessing features like ``--deskew`` are used, the
original PDF will be rendered as a full page and the OCR layer will be
placed on top.
``auto`` always selects ``hocr``.
The ``hocr`` renderer
---------------------
The ``hocr`` renderer works with older versions of Tesseract. The image
layer is copied from the original PDF page if possible, avoiding
potentially lossy transcoding or loss of other PDF information. If
preprocessing is specified, then the image layer is a new PDF. (You may
need to disable PDF/A conversion nad optimization to eliminate all
lossy transformations.)
.. versionchanged:: 16.0.0
Unlike ``sandwich`` this renderer is implemented within OCRmyPDF; anyone
looking to customize how OCR is presented should look here. A major
disadvantage of this renderer is it not capable of correctly handling
text outside the Latin alphabet (specifically, it supports the ISO 8859-1
character set). Pull requests to improve the situation are welcome.
In both renderers, a text-only layer is rendered and sandwiched (overlaid)
on to either the original PDF page, or newly rasterized version of the
original PDF page (when ``--force-ocr`` is used). In this way, loss
of PDF information is generally avoided. (You may need to disable PDF/A
conversion and optimization to eliminate all lossy transformations.)
Currently, this renderer has the best compatibility with Mozilla's
PDF.js viewer.
The current approach used by the new hOCR renderer is a re-implementation
of Tesseract's PDF renderer, using the same Glyphless font and general
ideas, but fixing many technical issues that impeded it. The new hocr
provides better text placement accuracy, avoids issues with word
segmentation, and provides better positioning of skewed text.
This works in all versions of Tesseract.
Using the experimental API, it is also possible to edit the OCR output
from Tesseract, using any tool that is capable of editing hOCR files.
Older versions of this renderer did not support non-Latin languages, but
it is now universal.
The ``sandwich`` renderer
-------------------------
The ``sandwich`` renderer uses Tesseract's text-only PDF feature,
which produces a PDF page that lays out the OCR in invisible text.
Currently some problematic PDF viewers like Mozilla PDF.js and macOS
Preview have problems with segmenting its text output, and
mightrunseveralwordstogether. It also does not implement right to left
fonts (Arabic, Hebrew, Persian). The output of this renderer cannot
be edited. The sandwich renderer is retained for testing.
When image preprocessing features like ``--deskew`` are used, the
original PDF will be rendered as a full page and the OCR layer will be
placed on top.
Rendering and rasterizing options
=================================
+3 -1
View File
@@ -112,7 +112,9 @@ OCRmyPDF is strict about not writing to standard output so that
users can safely use it in a pipeline and produce a valid output
file. A caller application will have to ensure it does not write to
standard output either, if it wants to be compatible with this
behavior and support piping to a file.
behavior and support piping to a file. Another benefit of running
OCRmyPDF in a child process, as recommended above, is that it will
not interfere with the parent process's standard output.
Exceptions
----------
+16 -2
View File
@@ -187,7 +187,7 @@ might remove desirable content, especially from poor quality scans.
background from grayscale or color images. Monochrome images are
ignored. This should not be used on documents that contain color
photos as it may remove them.
- ``--deskew`` will correct pages were scanned at a skewed angle by
- ``--deskew`` will correct pages that were scanned at a skewed angle by
rotating them back into place.
- ``--clean`` uses
`unpaper <https://www.flameeyes.eu/projects/unpaper>`__ to clean up
@@ -245,6 +245,20 @@ if all you want to is to apply image processing or PDF/A conversion.
the case. Use ``--tesseract-non-ocr-timeout`` to control the timeout
for non-OCR operations, if needed.
Remove all text or OCR from my PDF
----------------------------------
This is getting ridiculous, but OCRmyPDF can complete strip all textual
information from a PDF and reconstruct it as a "bag of images" PDF.
.. code-block::
ocrmypdf --tesseract-timeout 0 --force-ocr input.pdf output.pdf
Why would you want to do this? Perhaps you have a PDF where OCR
fails to produce useful results, and just want to get rid of all OCR information.
This command also removes OCR generated by third party tools.
Optimize images without performing OCR
--------------------------------------
@@ -393,4 +407,4 @@ any digital signatures will be invalidated.
OCRmyPDF cannot open documents that are encrypted with a digital certificate.
Versions of OCRmyPDF prior to 14.4.0 would invalidate existing digital signatures
without warning.
without warning.
+85 -47
View File
@@ -23,7 +23,9 @@ These platforms have one-liner installs:
+-------------------------------+-----------------------------------------+
| Fedora | ``dnf install ocrmypdf tesseract-osd`` |
+-------------------------------+-----------------------------------------+
| macOS | ``brew install ocrmypdf`` |
| macOS (Homebrew) | ``brew install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| macOS (MacPorts) | ``port install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
| LinuxBrew | ``brew install ocrmypdf`` |
+-------------------------------+-----------------------------------------+
@@ -99,12 +101,12 @@ For full details on version availability for your platform, check the
Fedora
------
.. |fedora-37| image:: https://repology.org/badge/version-for-repo/fedora_37/ocrmypdf.svg
:alt: Fedora 37
.. |fedora-38| image:: https://repology.org/badge/version-for-repo/fedora_38/ocrmypdf.svg
:alt: Fedora 38
.. |fedora-39| image:: https://repology.org/badge/version-for-repo/fedora_39/ocrmypdf.svg
:alt: Fedora 39
.. |fedora-rawhide| image:: https://repology.org/badge/version-for-repo/fedora_rawhide/ocrmypdf.svg
:alt: Fedore Rawhide
@@ -113,7 +115,7 @@ Fedora
+-----------------------------------------------+
| |latest| |
+-----------------------------------------------+
| |fedora-37| |fedora-38| |fedora-rawhide| |
| |fedora-38| |fedora-39| |fedora-rawhide| |
+-----------------------------------------------+
Users of Fedora may simply
@@ -123,7 +125,7 @@ Users of Fedora may simply
dnf install ocrmypdf tesseract-osd
For full details on version availability, check the `Fedora Package
Tracker <https://apps.fedoraproject.org/packages/ocrmypdf>`__.
Tracker <https://packages.fedoraproject.org/pkgs/ocrmypdf/ocrmypdf/>`__.
If the version available for your platform is out of date, you could opt
to install the latest version from source. See `Installing HEAD revision
@@ -135,10 +137,36 @@ from sources <#installing-head-revision-from-sources>`__.
issues. OCRmyPDF works fine without it but will produce larger output
files. If you build jbig2enc from source, ocrmypdf 7.0.0 and later
will automatically detect it on the ``PATH``. To add JBIG2 encoding,
see `Installing the JBIG2 encoder <jbig2>`__.
see :ref:`Installing the JBIG2 encoder <jbig2>`.
.. _ubuntu-lts-latest:
RHEL 9
------
Prepare the environment by getting Python 3.11:
.. code-block:: bash
dnf install python3.11 python3.11-pip
Then, follow `Requirements for pip and HEAD install <#requirements-for-pip-and-head-install>`__ to instal dependencies:
.. code-block:: bash
dnf install ghostscript tesseract
and build ocrmypdf in virtual environment:
.. code-block:: bash
python3.11 -m venv .venv
To add JBIG2 encoding, see :ref:`Installing the JBIG2 encoder <jbig2>`.
Note Fedora packages for language data haven't been branched for RHEL/EPEL, but you can get traineddata files directly from `tesseract
<https://github.com/tesseract-ocr/tessdata/>`__ and place them in ``/usr/share/tesseract/tessdata``.
Installing the latest version on Ubuntu 22.04 LTS
-------------------------------------------------
@@ -162,37 +190,13 @@ To add JBIG2 encoding, see :ref:`jbig2`.
Ubuntu 20.04 LTS
----------------
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. To
install a more recent version, uninstall the system-provided version of
ocrmypdf, and install the following dependencies:
Ubuntu 20.04 includes ocrmypdf 9.6.0 - you can install that with ``apt``. The
most convenient way to install recent OCRmyPDF on older Ubuntu is to use
Homebrew on Linux (Linuxbrew).
.. code-block:: bash
sudo apt-get -y remove ocrmypdf # remove system ocrmypdf, if installed
sudo apt-get -y update
sudo apt-get -y install \
ghostscript \
icc-profiles-free \
libxml2 \
pngquant \
python3-pip \
tesseract-ocr \
zlib1g
To install ocrmypdf for the system:
.. code-block:: bash
pip3 install ocrmypdf
To install for the current user only:
.. code-block:: bash
export PATH=$HOME/.local/bin:$PATH
pip3 install --user ocrmypdf
To add JBIG2 encoding, see :ref:`jbig2`.
brew install ocrmypdf
Arch Linux (AUR)
----------------
@@ -215,7 +219,7 @@ you are using a VM image, such as `the official Vagrant image
be completed for you.
Next you should install the `base-devel package group
<https://www.archlinux.org/groups/x86_64/base-devel/>`__. This includes the
<https://archlinux.org/packages/core/any/base-devel/>`__. This includes the
standard tooling needed to build packages, such as a compiler and binary tools.
.. code-block:: bash
@@ -258,7 +262,7 @@ page.
<https://aur.archlinux.org/packages/jbig2enc-git/>`__ and may be installed
using the same series of steps as for the installation OCRmyPDF AUR
package. Alternatively, it may be built manually from source following the
instructions in `Installing the JBIG2 encoder <jbig2>`__. If JBIG2 is
instructions in :ref:`Installing the JBIG2 encoder <jbig2>`. If JBIG2 is
installed, OCRmyPDF 7.0.0 and later will automatically detect it.
Alpine Linux
@@ -323,6 +327,22 @@ languages you can optionally install them all:
brew install tesseract-lang # Optional: Install all language packs
MacPorts
--------
.. image:: https://img.shields.io/badge/dynamic/json?url=https%3A%2F%2Fports.macports.org%2Fapi%2Fv1%2Fports%2Focrmypdf%2F%3Fformat%3Djson&query=version&label=MacPorts
:alt: Macports Version Information
:target: https://ports.macports.org/port/ocrmypdf
OCRmyPDF is includes in MacPorts:
.. code-block:: bash
sudo port install ocrmypdf
Note that while this will install tesseract you will need to install
the appropriate tesseract `language ports <https://ports.macports.org/search/?selected_facets=categories_exact%3Atextproc&installed_file=&q=tesseract&name=on>`__.
Manual installation on macOS
----------------------------
@@ -391,18 +411,20 @@ package manager:
* ``winget install -e --id Python.Python.3.11``
* ``winget install -e --id UB-Mannheim.TesseractOCR``
* ``winget install -e --id ArtifexSoftware.GhostScript``
You will need to install Ghostscript manually, `since it does not support automated
installs anymore <https://artifex.com/news/ghostscript-10.01.0-disabling-silent-install-option>`_.
* `Ghostscript download page <https://ghostscript.com/releases/gsdnld.html>`_.`
(Or alternately, using the `Chocolatey <https://chocolatey.org/>`_ package manager, install
the following when running in an Administrator command prompt):
* ``choco install python3``
* ``choco install --pre tesseract``
* ``choco install ghostscript``
* ``choco install pngquant`` (optional)
Either set of commands will install the required software. At the mmoment there is no
Either set of commands will install the required software. At the moment there is no
single command to install Windows.
You may then use ``pip`` to install ocrmypdf. (This can performed by a user or
@@ -452,7 +474,7 @@ Cygwin64
First install the the following prerequisite Cygwin packages using ``setup-x86_64.exe``::
python39 (or later)
python310 (or later)
python3?-devel
python3?-pip
python3?-lxml
@@ -538,9 +560,25 @@ try:
pip install --user ocrmypdf
(If the message appears ``Requirement already satisfied: ocrmypdf in...``,
you will need to use ``pip install --user --upgrade ocrmypdf``.)
You should then be able to run ``ocrmypdf --version`` and see that the
latest version was located.
Installing with pipx
====================
Some users may prefer pipx. As with the method above, you will need to
satisfy all non-Python dependencies. Then if pipx is installed, you
can use
.. code-block:: bash
pipx run ocrmypdf
(If not installed, pipx will install first.)
Requirements for pip and HEAD install
-------------------------------------
@@ -550,8 +588,8 @@ manager. ``pip`` cannot provide them.
The following versions are required:
- Python 3.9 or newer
- Ghostscript 9.55 or newer
- Python 3.10 or newer
- Ghostscript 9.54 or newer
- Tesseract 4.1.1 or newer
- jbig2enc 0.29 or newer
- pngquant 2.5 or newer
@@ -586,7 +624,7 @@ unfortunately, the ``pip install`` command cannot satisfy all of them.
Installing HEAD revision from sources
=====================================
If you have ``git`` and Python 3.9 or newer installed, you can install
If you have ``git`` and Python 3.10 or newer installed, you can install
from source. When the ``pip`` installer runs, it will alert you if
dependencies are missing.
@@ -603,7 +641,7 @@ environment:
pip install git+https://github.com/ocrmypdf/OCRmyPDF.git
Or, to install in `development
mode <https://pythonhosted.org/setuptools/setuptools.html#development-mode>`__,
mode <https://packaging.python.org/en/latest/guides/distributing-packages-using-setuptools/#working-in-development-mode>`__,
allowing customization of OCRmyPDF, use the ``-e`` flag:
.. code-block:: bash
@@ -669,7 +707,7 @@ To manually install the ``fish`` completion, copy
Note on 32-bit support
======================
Many Python libraries no longer 32-bit binary wheels for Linux. This
Many Python libraries no longer provide 32-bit binary wheels for Linux. This
includes many of the libraries that OCRmyPDF depends on, such as
Pillow. The easiest way to express this to end users is to say we don't
support 32-bit Linux.
@@ -679,4 +717,4 @@ can still install and use OCRmyPDF. A warning message will appear.
In practice, OCRmyPDF may need more than 32-bit memory space to run when
large documents are processed, so there are practical limitations to what
users can accomplish with it. Still, for the common use case of an 32-bit
ARM NAS or Raspberry Pi processing small documents, it should work.
ARM NAS or Raspberry Pi processing small documents, it should work.
+9 -1
View File
@@ -41,7 +41,15 @@ For all other platforms, you would need to build the JBIG2 encoder from source:
Dependencies include libtoolize and libleptonica, which on Ubuntu systems
are packaged as libtool and libleptonica-dev. On Fedora (35) they are packaged
as libtool and leptonica-devel.
as libtool and leptonica-devel. For this to work, please make sure to install
``autotools``, ``automake``, ``libtool`` and ``leptonica`` first if not already
installed.
.. code-block:: bash
[sudo] apt install autotools-dev automake libtool libleptonica-dev
..
Lossy mode JBIG2
================
+47 -13
View File
@@ -18,16 +18,26 @@ Tesseract's documentation also lists the three-letter code for your language.
Some are anglicized, e.g. Spanish is ``spa`` rather than ``esp``, while others
are not, e.g. German is ``deu`` and French is ``fra``.
Language packs (strictly speaking, Tesseract "traineddata" files) generally correspond
to the language in question, but different language packs are used in certain
situations. For German, the "Fraktur" language pack can assist with reading older
materials in the Fraktur typeface family (``deu_frak``). Some communities have changed
their script from Cyrillic to Latin; the Cyrillic version of Uzbek is available
as ``uzb_cyrl`` and the Latin version is ``uzb``.
After you have installed a language pack, you can use it with ``ocrmypdf -l <language>``,
for example ``ocrmypdf -l spa``. For multilingual documents, you can specify
all languages to be expected, e.g. ``ocrmypdf -l eng+fra`` for English and French.
English is assumed by default unless other language(s) are specified.
For Linux users, you can often find packages that provide language
packs:
packs.
Debian and Ubuntu users
=======================
Platform install steps
======================
Debian and Ubuntu (apt)
-----------------------
.. code-block:: bash
@@ -42,8 +52,8 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Fedora users
============
Fedora
------
.. code-block:: bash
@@ -58,8 +68,24 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Gentoo users
============
Archlinux
------
.. code-block:: bash
# Display a list of all Tesseract language packs
pacman -Ss tesseract-data
# Install German language pack
pacman -S tesseract-data-deu
You can then pass the ``-l LANG`` argument to OCRmyPDF to give a hint as
to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
Gentoo
------
On Gentoo the package ``app-text/tessdata_fast``, which ``app-text/tesseract`` depends on, handles Tesseract languages.
It accepts USE flags to select what languages should be installed, these can be set in ``/etc/portage/package.use``.
@@ -85,23 +111,31 @@ to what languages it should search for. Multiple languages can be
requested using either ``-l eng+fra`` (English and French) or
``-l eng -l fra``.
macOS users
===========
macOS
-----
You can install additional language packs by
:ref:`installing Tesseract using Homebrew with all language packs <macos-all-languages>`.
Docker users
============
Docker
------
Users of the OCRmyPDF Docker image should install language packs into a
derived Docker image as
:ref:`described in that section <docker-lang-packs>`.
Windows users
=============
Windows
-------
The Tesseract installer provided by Chocolatey currently includes only English language.
To install other languages, download the respective language pack (``.traineddata`` file)
from https://github.com/tesseract-ocr/tessdata/ and place it in
``C:\\Program Files\\Tesseract-OCR\\tessdata`` (or wherever Tesseract OCR is installed).
Custom language packs
=====================
If you have fine-tuned or trained Tesseract and generated custom trained data, you can
copy your ``customlang.traineddata`` file into your Tesseract "tessdata" folder, and
then use the ``-l customlang`` argument to tell OCRmyPDF to pass that language on to
Tesseract.
+10 -1
View File
@@ -64,4 +64,13 @@ installation documentation.
If you maintain a Linux distribution that supports 32-bit x86 or ARM, OCRmyPDF
should continue to work as long as all of its dependencies continue to be
available in 32-bit form. Please note we do not test on 32-bit platforms.
available in 32-bit form. Please note we do not test on 32-bit platforms.
HEIF/HEIC
---------
OCRmyPDF defaults to installing the pi-heif PyPI package, which supports converting
HEIF (High Efficiency Image File Format) images to PDF from the command line.
If your distribution does not have this library available, you can exclude it and
OCRmyPDF will gracefully degrade automatically, losing only support for this
feature.
+1 -1
View File
@@ -17,7 +17,7 @@ If running OCRmyPDF quickly is your main goal, you can use settings such as:
* ``--optimize 0`` to disable file size optimization
* ``--output-type pdf`` to disable PDF/A generation
* ``--fast-web-view 0`` to disable fast web view optimization
* ``--fast-web-view 999999`` to disable fast web view optimization
* ``--skip-big`` to skip large images, if some pages have large images
You can also avoid:
+119
View File
@@ -20,6 +20,8 @@ The most recent release of OCRmyPDF is |OCRmyPDF PyPI|. Any newer versions
referred to in these notes may exist the main branch but have not been
tagged yet.
OCRmyPDF typically supports the three most recent Python versions.
.. note::
Attention maintainers: these release notes may be updated with information
@@ -28,6 +30,123 @@ tagged yet.
.. |OCRmyPDF PyPI| image:: https://img.shields.io/pypi/v/ocrmypdf.svg
v16.3.1
=======
- Fixed a test suite failure with Ghostscript 10.03.0+. :issue:`1316`
- Fixed an issue with the presentation of the "OCR" progress bar. :issue:`1313`
v16.3.0
=======
- Fixed progress bar not displaying for Ghostscript PDF/A conversion. :issue:`1313`
- Added progress bar for linearization. :issue:`1313`
- If `--rotate-pages-threshold` issued without `--rotate-pages` we now exit with
an error since the user likely intended to use `--rotate-pages`. :issue:`1309`
- If Tesseract hOCR gives an invalid line box, print an error message instead of
exiting with an error. :issue:`1312`
v16.2.0
=======
- Fixed issue 'NoneType' object has no attribute 'get' when optimizing certain PDFs.
:issue:`1293,1271`
- Switched formatting from black to ruff.
- Added support for sending sidecar output to io.BytesIO.
- Added support for converting HEIF/HEIC images (the native image of iPhones and
some other devices) to PDFs, when the appropriate pi-hief library is installed.
This library is marked as a dependency, but maintainers may opt out if needed.
- We now default to downsampling large images that would exceed Tesseract's internal
limits, but only if it cause processing to fail. Previously, this behavior only
occurred if specifically requested on command line. It can still be configured
and disabled. See the --tesseract command line options.
- Added Macports install instructions. Thanks @akierig.
- Improved logging output when an unexpected error occurs while trying to obtain
the version of a third party program.
v16.1.2
=======
- Fixed test suite failure when using Ghostscript 10.3.
- Other minor corrections.
v16.1.1
=======
- Fixed PyPy 3.10 support.
v16.1.0
=======
- Improved hOCR renderer is now default for left to right languages.
- Improved handling of rotated pages. Previously, OCR text might be missing for
pages that were rotated with a /Rotate tag on the page entry.
- Improved handling of cropped pages. Previously, in some cases a page with a
crop box would not have its OCR applied correctly and misalignment between
OCR text and visible text coudl occur.
- Documentation improvements, especially installation instructions for less
common platforms.
v16.0.4
=======
- Fixed some issues for left-to-right text with the new hOCR renderer. It is still
not default yet but will be made so soon. Right-to-left text is still in progress.
- Added an error to prevent use of several versions of Ghostscript that seem
corrupt existing text in input PDFs. Newly generated OCR is not affected.
For best results, use Ghostscript 10.02.1 or newer, which contains the fix
for the issue.
v16.0.3
=======
- Changed minimum required Ghostscript to 9.54, to support users of RHEL 9 and its
derivatives, since that is the latest version available there.
- Removed warning message about CVE-2023-43115, on the assumption that most
distributions have backported the patch by now.
v16.0.2
=======
- Temporarily changed PDF text renderer back to sandwich by default to address
regressions in macOS Preview.
v16.0.1
=======
- Fixed text rendering issue with new hOCR text renderer - extraneous byte order
marks.
- Tightened dependencies.
v16.0.0
=======
- Added OCR text renderer, combined the best ideas of Tesseract's PDF
generator and the older hOCR transformer renderer. The result is a hopefully
permanent fix for wordssmushedtogetherwithoutspaces issues in extracted text,
better registration/position of text on skewed baselines :issue:`1009`,
fixes to character output when the German Fraktur script is used :issue:`1191`,
proper rendering of right to left languages (Arabic, Hebrew, Persian) :issue:`1157`.
Asian languages may still have excessive word breaks compared to expectations.
The new renderer is the default; the old sandwich renderer is still available
using ``--pdf-renderer sandwich``; the old hOCR renderer is no more.
- The ``ocrmypdf.hocrtransform`` API has changed substantially.
- Support for Python 3.9 has been dropped. Python 3.10+ is now required.
- pikepdf >= 8.8.0 is now required.
v15.4.4
=======
- Fixed documentation for installing Ghostscript on Windows. :issue:`1198`
- Added warning message about security issue in older versions of Ghostscript.
v15.4.3
=======
- Fixed deprecation warning in pikepdf older than 8.7.1; pikepdf >= 8.7.1 is
now required.
v15.4.2
=======
+48 -10
View File
@@ -1,5 +1,6 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2016 findingorder <https://github.com/findingorder>
# SPDX-FileCopyrightText: 2024 nilsro <https://github.com/nilsro>
# SPDX-License-Identifier: MIT
"""Example of using ocrmypdf as a library in a script.
@@ -15,6 +16,10 @@ from __future__ import annotations
import logging
import sys
import os
import posixpath
import shutil
import filecmp
from pathlib import Path
import ocrmypdf
@@ -22,32 +27,65 @@ import ocrmypdf
# pylint: disable=logging-format-interpolation
# pylint: disable=logging-not-lazy
def filecompare(a, b):
try:
return filecmp.cmp(a, b, shallow=True)
except FileNotFoundError:
return False
script_dir = Path(__file__).parent
# set archive_dir to a path for backup original documents. Leave empty if not required.
archive_dir = "/pdfbak"
if len(sys.argv) > 1:
start_dir = Path(sys.argv[1])
else:
start_dir = Path('.')
start_dir = Path(".")
if len(sys.argv) > 2:
log_file = Path(sys.argv[2])
else:
log_file = script_dir.with_name('ocr-tree.log')
log_file = script_dir.with_name("ocr-tree.log")
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s %(message)s',
format="%(asctime)s %(message)s",
filename=log_file,
filemode='a',
filemode="a",
)
logging.info(f"Start directory {start_dir}")
ocrmypdf.configure_logging(ocrmypdf.Verbosity.default)
for filename in start_dir.glob("**/*.py"):
for filename in start_dir.glob("**/*.pdf"):
logging.info(f"Processing {filename}")
result = ocrmypdf.ocr(filename, filename, deskew=True)
if result == ocrmypdf.ExitCode.already_done_ocr:
logging.error("Skipped document because it already contained text")
elif result == ocrmypdf.ExitCode.ok:
if ocrmypdf.pdfa.file_claims_pdfa(filename)["pass"]:
logging.info("Skipped document because it already contained text")
else:
archive_filename = archive_dir + str(filename)
if len(archive_dir) > 0 and not filecompare(filename, archive_filename):
logging.info(f"Archiving document to {archive_filename}")
try:
shutil.copy2(filename, posixpath.dirname(archive_filename))
except IOError as io_err:
os.makedirs(posixpath.dirname(archive_filename))
shutil.copy2(filename, posixpath.dirname(archive_filename))
try:
result = ocrmypdf.ocr(filename, filename, deskew=True)
logging.info(result)
except ocrmypdf.exceptions.EncryptedPdfError:
logging.info("Skipped document because it is encrypted")
except ocrmypdf.exceptions.PriorOcrFoundError:
logging.info("Skipped document because it already contained text")
except ocrmypdf.exceptions.DigitalSignatureError:
logging.info("Skipped document because it has a digital signature")
except ocrmypdf.exceptions.TaggedPDFError:
logging.info(
"Skipped document because it does not need ocr as it is tagged"
)
except:
logging.error("Unhandled error occured")
logging.info("OCR complete")
logging.info(result)
+4 -3
View File
@@ -53,9 +53,10 @@ for dir_name, _subdirs, file_list in os.walk(start_dir):
]
logging.info(cmd)
full_path_ocr = os.path.join(dir_name, filename_ocr)
with open(filename, 'rb') as input_file, open(
full_path_ocr, 'wb'
) as output_file:
with (
open(filename, 'rb') as input_file,
open(full_path_ocr, 'wb') as output_file,
):
proc = subprocess.run(
cmd,
stdin=input_file,
+2 -3
View File
@@ -7,7 +7,6 @@
# Do not enable annotations!
# https://github.com/tiangolo/typer/discussions/598
# from __future__ import annotations
import json
import logging
@@ -131,7 +130,7 @@ def execute_ocrmypdf(
class HandleObserverEvent(PatternMatchingEventHandler):
def __init__(
def __init__( # noqa: D107
self,
patterns=None,
ignore_patterns=None,
@@ -191,7 +190,7 @@ def main(
bool,
typer.Option(
envvar='OCR_OUTPUT_DIRECTORY_YEAR_MONTH',
help='Create a subdirectory in the output directory for each year and month',
help='Create a subdirectory in the output directory for each year/month',
),
] = False,
on_success_delete: Annotated[
+24 -39
View File
@@ -10,18 +10,17 @@ dynamic = ["version"]
description = "OCRmyPDF adds an OCR text layer to scanned PDF files, allowing them to be searched"
readme = "README.md"
license = { text = "MPL-2.0" }
requires-python = ">=3.9"
requires-python = ">=3.10"
dependencies = [
"Pillow>=10.0.1",
"deprecation>=2.1.0",
"img2pdf>=0.4.4",
"img2pdf>=0.5",
"packaging>=20",
"pdfminer.six>=20220319",
"pikepdf>=8",
"pluggy>=0.13.0",
"reportlab>=3.6.8",
"pi-heif", # Heif image format - maintainers: if this is removed, it will NOT break
"pikepdf>=8.10.1",
"Pillow>=10.0.1",
"pluggy>=1",
"rich>=13",
"typing-extensions>=4;python_version<'3.10'",
]
authors = [{ name = "James R. Barlow", email = "james@purplerock.ca" }]
classifiers = [
@@ -58,10 +57,11 @@ test = [
"pytest-cov>=3.0.0",
"pytest-xdist>=2.5.0",
"python-xmp-toolkit==2.0.1", # also requires apt-get install libexempi3
"reportlab>=3.6.8",
"types-Pillow",
"types-humanfriendly",
]
watcher = ["watchdog>=1.0.2", "typer[all]", "python-dotenv"]
watcher = ["watchdog>=1.0.2", "typer-slim[standard]", "python-dotenv"]
webservice = ["Flask>=2.0.1"]
[project.scripts]
@@ -77,30 +77,7 @@ namespaces = false
[tool.setuptools_scm]
[tool.distutils.bdist_wheel]
python-tag = "py39"
[tool.black]
line-length = 88
target-version = ["py39", "py310", "py311"]
skip-string-normalization = true
include = '\.pyi?$'
exclude = '''
/(
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
| docs
| misc
| \.egg-info
)/
'''
python-tag = "py310"
[tool.coverage.run]
branch = true
@@ -132,7 +109,11 @@ norecursedirs = ["lib", ".pc", ".git", "venv", "output", "cache", "resources"]
testpaths = ["tests"]
addopts = "-n auto"
markers = ["slow"]
filterwarnings = ["ignore:.*XMLParser.*:DeprecationWarning"]
filterwarnings = [
"ignore:.*XMLParser.*:DeprecationWarning",
"ignore:.*ast.NameConstant.*:DeprecationWarning:reportlab",
"ignore:.*distutils.*:DeprecationWarning:libxmp",
]
[tool.mypy]
@@ -150,7 +131,10 @@ module = [
ignore_missing_imports = true
[tool.ruff]
select = [
target-version = "py310"
[tool.ruff.lint]
"select" = [
"D", # pydocstyle
"E", # pycodestyle
"W", # pycodestyle
@@ -158,17 +142,18 @@ select = [
"I001", # isort
"UP", # pyupgrade
]
target-version = "py39"
[tool.ruff.isort]
[tool.ruff.lint.isort]
known-first-party = ["ocrmypdf"]
required-imports = ["from __future__ import annotations"]
[tool.ruff.pydocstyle]
[tool.ruff.lint.pydocstyle]
convention = "google"
[tool.ruff.per-file-ignores]
[tool.ruff.lint.per-file-ignores]
"docs/conf.py" = ["D100", "D101", "D105"]
"tests/*.py" = ["D100", "D101", "D102", "D103", "D105"]
"misc/*.py" = ["D103", "D101", "D102"]
"src/ocrmypdf/builtin_plugins/*.py" = ["D103", "D102", "D105"]
[tool.ruff.format]
quote-style = "preserve"
+7 -3
View File
@@ -7,8 +7,8 @@ from __future__ import annotations
import threading
from abc import ABC, abstractmethod
from collections.abc import Iterable
from typing import Callable, TypeVar
from collections.abc import Callable, Iterable
from typing import Any, TypeVar
from ocrmypdf._progressbar import NullProgressBar, ProgressBar
@@ -19,6 +19,10 @@ def _task_noop(*_args, **_kwargs):
return
def _task_finished_noop(_result: Any, pbar: ProgressBar):
pbar.update()
class Executor(ABC):
"""Abstract concurrent executor."""
@@ -66,7 +70,7 @@ class Executor(ABC):
if not worker_initializer:
worker_initializer = _task_noop
if not task_finished:
task_finished = _task_noop
task_finished = _task_finished_noop
if not task:
task = _task_noop
+8
View File
@@ -0,0 +1,8 @@
# SPDX-FileCopyrightText: 2024 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
# Enforce English hegemony
DEFAULT_LANGUAGE = 'eng'
# Default rotation threshold
DEFAULT_ROTATE_PAGES_THRESHOLD = 14.0
+18 -3
View File
@@ -177,6 +177,17 @@ class GhostscriptFollower:
self.progressbar_class = progressbar_class
self.progressbar = None
def __enter__(self):
# We can't actually set up the progressbar here, because we don't know
# how many pages there are until the first __call__() happens. So we
# do it in __call__().
return self
def __exit__(self, exc_type, exc_value, traceback):
if self.progressbar:
return self.progressbar.__exit__(exc_type, exc_value, traceback)
return False
def __call__(self, line):
if not self.progressbar_class:
return
@@ -187,7 +198,8 @@ class GhostscriptFollower:
self.progressbar = self.progressbar_class(
total=self.count, desc="PDF/A conversion", unit='page'
)
return
# Now that we know the count, we can set up the progressbar.
self.progressbar.__enter__()
else:
if self.re_page.match(line.strip()):
self.progressbar.update()
@@ -265,7 +277,10 @@ def generate_pdfa(
)
args_gs.extend(fspath(s) for s in pdf_pages) # Stringify Path objs
try:
with Path(output_file).open('wb') as output:
with (
Path(output_file).open('wb') as output,
GhostscriptFollower(progressbar_class) as pbar,
):
p = run_polling_stderr(
args_gs,
stdout=output,
@@ -274,7 +289,7 @@ def generate_pdfa(
text=True,
encoding='utf-8',
errors='replace',
callback=GhostscriptFollower(progressbar_class),
callback=pbar,
)
except CalledProcessError as e:
# Ghostscript does not change return code when it fails to create
+2 -1
View File
@@ -220,7 +220,8 @@ def get_deskew(
def tesseract_log_output(stream: bytes) -> None:
tlog = TesseractLoggerAdapter(
log, extra=log.extra if hasattr(log, 'extra') else None # type: ignore
log,
extra=log.extra if hasattr(log, 'extra') else None, # type: ignore
)
if not stream:
+3 -25
View File
@@ -8,48 +8,26 @@ from __future__ import annotations
import logging
import os
import shlex
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from decimal import Decimal
from pathlib import Path
from subprocess import PIPE, STDOUT
from typing import Union
from tempfile import TemporaryDirectory
from packaging.version import Version
from PIL import Image
from ocrmypdf.exceptions import MissingDependencyError, SubprocessOutputError
from ocrmypdf.exceptions import SubprocessOutputError
from ocrmypdf.subprocess import get_version, run
# unpaper documentation:
# https://github.com/Flameeyes/unpaper/blob/main/doc/basic-concepts.md
if sys.version_info >= (3, 10):
from tempfile import TemporaryDirectory
else:
from tempfile import TemporaryDirectory as _TemporaryDirectory
class TemporaryDirectory(_TemporaryDirectory):
"""Shim to consume ignore_cleanup_errors kwarg on Python 3.9 and older.
The argument is consumed without action. If users are getting errors related
to temporary file cleanup, they should upgrade to Python 3.10 which properly
cleans up temporary directories on Windows.
See: https://github.com/python/cpython/pull/24793
"""
def __init__(self, ignore_cleanup_errors=False, **kwargs):
super().__init__(**kwargs)
del _TemporaryDirectory
UNPAPER_IMAGE_PIXEL_LIMIT = 256 * 1024 * 1024
DecFloat = Union[Decimal, float]
DecFloat = Decimal | float
log = logging.getLogger(__name__)
+16 -8
View File
@@ -12,12 +12,12 @@ from pathlib import Path
from pikepdf import (
Dictionary,
Matrix,
Name,
Operator,
Page,
Pdf,
PdfError,
PdfMatrix,
Stream,
parse_content_stream,
unparse_content_stream,
@@ -268,13 +268,13 @@ class OcrGrafter:
mediabox = base_page.mediabox
wp, hp = mediabox[2] - mediabox[0], mediabox[3] - mediabox[1]
translate = PdfMatrix().translated(-wt / 2, -ht / 2)
untranslate = PdfMatrix().translated(wp / 2, hp / 2)
corner = PdfMatrix().translated(mediabox[0], mediabox[1])
translate = Matrix().translated(-wt / 2, -ht / 2)
untranslate = Matrix().translated(wp / 2, hp / 2)
corner = Matrix().translated(mediabox[0], mediabox[1])
# -rotation because the input is a clockwise angle and this formula
# uses CCW
text_rotation = -text_rotation % 360
rotate = PdfMatrix().rotated(text_rotation)
rotate = Matrix().rotated(text_rotation)
# Because of rounding of DPI, we might get a text layer that is not
# identically sized to the target page. Scale to adjust. Normally this
@@ -285,12 +285,13 @@ class OcrGrafter:
scale_y = hp / ht
# log.debug('%r', scale_x, scale_y)
scale = PdfMatrix().scaled(scale_x, scale_y)
scale = Matrix().scaled(scale_x, scale_y)
# Translate the text so it is centered at (0, 0), rotate it there, adjust
# for a size different between initial and text PDF, then untranslate, and
# finally move the lower left corner to match the mediabox
# finally move the lower left corner to match the mediabox.
ctm = translate @ rotate @ scale @ untranslate @ corner
log.debug("Grafting with ctm %r", ctm)
base_resources = _ensure_dictionary(base_page.obj, Name.Resources)
base_xobjs = _ensure_dictionary(base_resources, Name.XObject)
@@ -310,9 +311,16 @@ class OcrGrafter:
if strip_old_text:
strip_invisible_text(self.pdf_base, base_page)
base_page.contents_coalesce()
if self.render_mode == RenderMode.ON_TOP:
# Add q/Q to ensure content we append is drawn correctly
# Strictly speaking this needs to trace the whole q/Q stack in case
# stack is not balanced.
original = base_page.Contents.read_bytes()
base_page.Contents.write(b'q\n' + original + b'\nQ\n')
base_page.contents_add(
new_text_layer, prepend=self.render_mode == RenderMode.UNDERNEATH
)
base_page.contents_coalesce()
_update_resources(obj=base_page.obj, font=font, font_key=font_key)
+2 -2
View File
@@ -93,8 +93,8 @@ class PageContext:
state = self.__dict__.copy()
state['options'] = copy(self.options)
if not isinstance(state['options'].input_file, (str, bytes, os.PathLike)):
if not isinstance(state['options'].input_file, str | bytes | os.PathLike):
state['options'].input_file = 'stream'
if not isinstance(state['options'].output_file, (str, bytes, os.PathLike)):
if not isinstance(state['options'].output_file, str | bytes | os.PathLike):
state['options'].output_file = 'stream'
return state
+37 -7
View File
@@ -153,22 +153,52 @@ def _set_language(pdf: Pdf, languages: list[str]):
pdf.Root.Lang = iso639_2
class MetadataProgress:
def __init__(self, progressbar_class):
self.progressbar_class = progressbar_class
self.progressbar = self.progressbar_class(
total=100, desc="Linearizing", unit='%'
)
def __enter__(self):
self.progressbar.__enter__()
return self
def __exit__(self, exc_type, exc_value, traceback):
return self.progressbar.__exit__(exc_type, exc_value, traceback)
def __call__(self, percent: int):
if not self.progressbar_class:
return
self.progressbar.update(completed=percent)
def metadata_fixup(
working_file: Path, context: PdfContext, pdf_save_settings: dict[str, Any]
) -> Path:
"""Fix certain metadata fields after Ghostscript PDF/A conversion.
"""Fix certain metadata fields whether PDF or PDF/A.
Override some of Ghostscript's metadata choices.
Also report on metadata in the input file that was not retained during
PDF/A conversion.
conversion.
"""
output_file = context.get_path('metafix.pdf')
options = context.options
with Pdf.open(context.origin) as original, Pdf.open(working_file) as pdf:
pbar_class = context.plugin_manager.hook.get_progressbar_class()
with (
Pdf.open(context.origin) as original,
Pdf.open(working_file) as pdf,
MetadataProgress(pbar_class) as pbar,
):
docinfo = get_docinfo(original, context)
with original.open_metadata(
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
) as meta_original, pdf.open_metadata() as meta_pdf:
with (
original.open_metadata(
set_pikepdf_as_editor=False, update_docinfo=False, strict=False
) as meta_original,
pdf.open_metadata() as meta_pdf,
):
meta_pdf.load_from_docinfo(
docinfo, delete_missing=False, raise_failure=False
)
@@ -179,6 +209,6 @@ def metadata_fixup(
report_on_metadata(options, meta_missing)
_set_language(pdf, options.languages)
pdf.save(output_file, **pdf_save_settings)
pdf.save(output_file, progress=pbar, **pdf_save_settings)
return output_file
+100 -22
View File
@@ -12,8 +12,9 @@ import re
import sys
from collections.abc import Iterable, Iterator, Sequence
from contextlib import suppress
from io import BytesIO
from pathlib import Path
from shutil import copyfileobj, copystat
from shutil import copyfileobj
from typing import Any, BinaryIO, TypeVar, cast
import img2pdf
@@ -34,17 +35,29 @@ from ocrmypdf.exceptions import (
UnsupportedImageFormatError,
)
from ocrmypdf.helpers import IMG2PDF_KWARGS, Resolution, safe_symlink
from ocrmypdf.hocrtransform import HocrTransform
from ocrmypdf.hocrtransform import DebugRenderOptions, HocrTransform
from ocrmypdf.hocrtransform._font import Courier
from ocrmypdf.pdfa import generate_pdfa_ps
from ocrmypdf.pdfinfo import Colorspace, Encoding, PageInfo, PdfInfo
from ocrmypdf.pluginspec import OrientationConfidence
try:
from pi_heif import register_heif_opener
except ImportError:
def register_heif_opener():
pass
T = TypeVar("T")
log = logging.getLogger(__name__)
VECTOR_PAGE_DPI = 400
register_heif_opener()
def triage_image_file(input_file: Path, output_file: Path, options) -> None:
"""Triage the input image file.
@@ -129,7 +142,7 @@ def _pdf_guess_version(input_file: Path, search_window=1024) -> str:
"""
with open(input_file, 'rb') as f:
signature = f.read(search_window)
m = re.search(br'%PDF-(\d\.\d)', signature)
m = re.search(rb'%PDF-(\d\.\d)', signature)
if m:
return m.group(1).decode('ascii')
return ''
@@ -712,19 +725,29 @@ def create_pdf_page_from_image(
pageinfo = page_context.pageinfo
pagesize = 72.0 * float(pageinfo.width_inches), 72.0 * float(pageinfo.height_inches)
effective_rotation = (pageinfo.rotation - orientation_correction) % 360
if effective_rotation % 180 == 90:
swap_axis = effective_rotation % 180 == 90
if swap_axis:
pagesize = pagesize[1], pagesize[0]
# This create a single page PDF
with open(image, 'rb') as imfile, open(output_file, 'wb') as pdf:
# Create a new single page PDF to hold
bio = BytesIO()
with open(image, 'rb') as imfile:
log.debug('convert')
layout_fun = img2pdf.get_layout_fun(pagesize)
img2pdf.convert(
imfile, layout_fun=layout_fun, outputstream=pdf, **IMG2PDF_KWARGS
imfile,
layout_fun=layout_fun,
outputstream=bio,
engine=img2pdf.Engine.pikepdf,
rotation=img2pdf.Rotation.ifvalid,
)
log.debug('convert done')
# img2pdf does not generate boxes correctly, so we fix them
bio.seek(0)
fix_pagepdf_boxes(bio, output_file, page_context, swap_axis=swap_axis)
output_file = page_context.plugin_manager.hook.filter_pdf_page(
page=page_context, image_filename=image, output_pdf=output_file
)
@@ -741,15 +764,27 @@ def render_hocr_page(hocr: Path, page_context: PageContext) -> Path:
return output_file
dpi = get_page_square_dpi(page_context, calculate_image_dpi(page_context))
debug_mode = options.pdf_renderer == 'hocrdebug'
hocrtransform = HocrTransform(hocr_filename=hocr, dpi=dpi.to_scalar()) # square
hocrtransform.to_pdf(
debug_kwargs = {}
if options.pdf_renderer == 'hocrdebug':
debug_kwargs = dict(
debug_render_options=DebugRenderOptions(
render_baseline=True,
render_triangle=True,
render_line_bbox=False,
render_word_bbox=True,
render_paragraph_bbox=False,
render_space_bbox=False,
),
font=Courier(),
)
HocrTransform(
hocr_filename=hocr,
dpi=dpi.to_scalar(),
**debug_kwargs, # square
).to_pdf(
out_filename=output_file,
image_filename=None,
show_bounding_boxes=False if not debug_mode else True,
invisible_text=True if not debug_mode else False,
interword_spaces=True,
invisible_text=True if not debug_kwargs else False,
)
return output_file
@@ -769,7 +804,57 @@ def ocr_engine_textonly_pdf(
output_text=output_text,
options=options,
)
return (output_pdf, output_text)
return output_pdf, output_text
def _offset_rect(rect: tuple[float, float, float, float], offset: tuple[float, float]):
"""Offset a rectangle by a given amount."""
return (
rect[0] + offset[0],
rect[1] + offset[1],
rect[2] + offset[0],
rect[3] + offset[1],
)
def fix_pagepdf_boxes(
infile: Path | BinaryIO,
out_file: Path,
page_context: PageContext,
swap_axis: bool = False,
) -> Path:
"""Fix the bounding boxes in a single page PDF.
The single page PDF is created with a normal MediaBox with its lower left corner
at (0, 0). infile is the single page PDF. page_context.mediabox has the original
file's mediabox, which may have a different origin. We needto adjust the other
boxes in the single page PDF to match the effect they had on the original page.
When correcting page rotation, we create a single page PDF that is correctly
rotated instead of an incorrectly rotated and then setting page.Rotate on it.
If rotation is either 90 or 270 degrees, then this function can be called
with swap_axis to swap the X and Y coordinates of all the boxes.
We are not concerned with solving degenerate cases where the boxes overlap or
or express invalid rectangles. We merely pass the boxes, producing a
transformation equivalent to the change made by constructing a new page image.
"""
with pikepdf.open(infile) as pdf:
for page in pdf.pages:
# page.BleedBox = page_context.pageinfo.bleedbox
# page.ArtBox = page_context.pageinfo.artbox
mediabox = page_context.pageinfo.mediabox
offset = mediabox[0], mediabox[1]
cropbox = _offset_rect(page_context.pageinfo.cropbox, offset)
trimbox = _offset_rect(page_context.pageinfo.trimbox, offset)
if swap_axis:
cropbox = cropbox[1], cropbox[0], cropbox[3], cropbox[2]
trimbox = trimbox[1], trimbox[0], trimbox[3], trimbox[2]
page.CropBox = cropbox
page.TrimBox = trimbox
pdf.save(out_file)
return pdf
def generate_postscript_stub(context: PdfContext) -> Path:
@@ -992,10 +1077,3 @@ def copy_final(
# get the appropriate umask, ownership, etc.
with open(output_file, 'w+b') as output_stream:
copyfileobj(input_stream, output_stream)
# Attempt to copy file attributes from input to output
if original_file:
with suppress(OSError):
# Copy original file's permissions, ownership, etc. if possible
copystat(original_file, output_file)
# Set output file's modification time to now
Path(output_file).touch(exist_ok=True)
+2 -2
View File
@@ -11,13 +11,13 @@ import os
import shutil
import sys
import threading
from collections.abc import Sequence
from collections.abc import Callable, Sequence
from concurrent.futures.process import BrokenProcessPool
from concurrent.futures.thread import BrokenThreadPool
from contextlib import contextmanager
from dataclasses import dataclass
from pathlib import Path
from typing import Callable, NamedTuple, cast
from typing import NamedTuple, cast
import PIL
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
+10 -12
View File
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
@@ -80,7 +79,6 @@ def _exec_page_sync(page_context: PageContext) -> PageResult:
page_context
)
ocr_out, text_out = _image_to_ocr_text(page_context, ocr_image_out)
return PageResult(
pageno=page_context.pageno,
pdf_page_from_image=pdf_page_from_image_out,
@@ -105,14 +103,14 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
try:
set_thread_pageno(result.pageno + 1)
sidecars[result.pageno] = result.text
pbar.update()
pbar.update(0.5)
ocrgraft.graft_page(
pageno=result.pageno,
image=result.pdf_page_from_image,
textpdf=result.ocr,
autorotate_correction=result.orientation_correction,
)
pbar.update()
pbar.update(0.5)
finally:
set_thread_pageno(None)
@@ -120,10 +118,9 @@ def exec_concurrent(context: PdfContext, executor: Executor) -> Sequence[str]:
use_threads=options.use_threads,
max_workers=max_workers,
progress_kwargs=dict(
total=(2 * len(context.pdfinfo)),
total=len(context.pdfinfo),
desc='OCR' if options.tesseract_timeout > 0 else 'Image processing',
unit='page',
unit_scale=0.5,
disable=not options.progress_bar,
),
worker_initializer=partial(worker_init, PIL.Image.MAX_IMAGE_PIXELS),
@@ -156,12 +153,13 @@ def _run_pipeline(
options: argparse.Namespace,
plugin_manager: OcrmypdfPluginManager,
) -> ExitCode:
with manage_work_folder(
work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")),
retain=options.keep_temporary_files,
print_location=options.keep_temporary_files,
) as work_folder, manage_debug_log_handler(
options=options, work_folder=work_folder
with (
manage_work_folder(
work_folder=Path(mkdtemp(prefix="ocrmypdf.io.")),
retain=options.keep_temporary_files,
print_location=options.keep_temporary_files,
) as work_folder,
manage_debug_log_handler(options=options, work_folder=work_folder),
):
executor = setup_pipeline(options, plugin_manager)
check_requested_output_file(options)
-1
View File
@@ -4,7 +4,6 @@
"""Implements the concurrent and page synchronous parts of the pipeline."""
from __future__ import annotations
import argparse
+11 -5
View File
@@ -66,7 +66,7 @@ class ProgressBar(Protocol):
def __exit__(self, *args):
"""Exit a progress bar context."""
def update(self, n=1):
def update(self, n=1, *, completed=None):
"""Update the progress bar by an increment.
For use within a progress bar context.
@@ -85,7 +85,7 @@ class NullProgressBar:
def __exit__(self, exc_type, exc_value, traceback):
return False
def update(self, _arg=None):
def update(self, _arg=None, *, completed=None):
return
@@ -103,6 +103,7 @@ class RichProgressBar:
disable: bool = False,
**kwargs,
):
self._entered = False
self.progress = Progress(
TextColumn(
"[progress.description]{task.description}",
@@ -130,6 +131,7 @@ class RichProgressBar:
def __enter__(self):
self.progress.start()
self._entered = True
return self
def __exit__(self, exc_type, exc_value, traceback):
@@ -137,6 +139,10 @@ class RichProgressBar:
self.progress.stop()
return False
def update(self, value=None):
advance = self.unit_scale if value is None else value
self.progress.update(self.progress_bar, advance=advance)
def update(self, n=1, *, completed=None):
assert self._entered, "Progress bar must be entered before updating"
if completed is None:
advance = self.unit_scale if n is None else n
self.progress.update(self.progress_bar, advance=advance)
else:
self.progress.update(self.progress_bar, completed=completed)
+6 -18
View File
@@ -20,6 +20,7 @@ import pikepdf
import PIL
from pluggy import PluginManager
from ocrmypdf._defaults import DEFAULT_LANGUAGE, DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._exec import unpaper
from ocrmypdf.exceptions import (
BadArgsError,
@@ -28,20 +29,11 @@ from ocrmypdf.exceptions import (
OutputFileAccessError,
)
from ocrmypdf.helpers import is_file_writable, monotonic, safe_symlink
from ocrmypdf.hocrtransform import HOCR_OK_LANGS
from ocrmypdf.subprocess import check_external_program
# -------------
# External dependencies
DEFAULT_LANGUAGE = 'eng' # Enforce English hegemony
log = logging.getLogger(__name__)
# --------
def check_platform() -> None:
if sys.maxsize <= 2**32: # pragma: no cover
log.warning(
@@ -83,15 +75,6 @@ def check_options_languages(
def check_options_output(options: Namespace) -> None:
is_latin = set(options.languages).issubset(HOCR_OK_LANGS)
if options.pdf_renderer.startswith('hocr') and not is_latin:
log.warning(
"The 'hocr' PDF renderer is known to cause problems with one "
"or more of the languages in your document. Use "
"`--pdf-renderer auto` (the default) to avoid this issue."
)
if options.output_type == 'none' and options.output_file not in (os.devnull, '-'):
raise BadArgsError(
"Since you specified `--output-type none`, the output file "
@@ -140,6 +123,11 @@ def check_options_preprocessing(options: Namespace) -> None:
options.clean = True
if options.unpaper_args and not options.clean:
raise BadArgsError("--clean is required for --unpaper-args")
if (
options.rotate_pages_threshold != DEFAULT_ROTATE_PAGES_THRESHOLD
and not options.rotate_pages
):
raise BadArgsError("--rotate-pages is required for --rotate-pages-threshold")
if options.clean:
check_external_program(
program='unpaper',
+13 -8
View File
@@ -14,7 +14,7 @@ from collections.abc import Iterable, Sequence
from enum import IntEnum
from io import IOBase
from pathlib import Path
from typing import AnyStr, BinaryIO, Union
from typing import AnyStr, BinaryIO
from warnings import warn
import pluggy
@@ -28,8 +28,8 @@ from ocrmypdf._validation import check_options
from ocrmypdf.cli import ArgumentParser, get_parser
from ocrmypdf.helpers import is_iterable_notstr
StrPath = Union[Path, AnyStr]
PathOrIO = Union[BinaryIO, StrPath]
StrPath = Path | AnyStr
PathOrIO = BinaryIO | StrPath
# Installing plugins affects the global state of the Python interpreter,
# so we need to use a lock to prevent multiple threads from installing
@@ -169,7 +169,7 @@ def _kwargs_to_cmdline(
# We have a parameter
cmdline.append(f"--{cmd_style_arg}")
if isinstance(val, (int, float)):
if isinstance(val, int | float):
cmdline.append(str(val))
elif isinstance(val, str):
cmdline.append(val)
@@ -201,14 +201,17 @@ def create_options(
defer_kwargs={'progress_bar', 'plugins', 'parser', 'input_file', 'output_file'},
**kwargs,
)
if isinstance(input_file, (BinaryIO, IOBase)):
if isinstance(input_file, BinaryIO | IOBase):
cmdline.append('stream://input_file')
else:
cmdline.append(os.fspath(input_file))
if isinstance(output_file, (BinaryIO, IOBase)):
if isinstance(output_file, BinaryIO | IOBase):
cmdline.append('stream://output_file')
else:
cmdline.append(os.fspath(output_file))
if 'sidecar' in kwargs and isinstance(kwargs['sidecar'], BinaryIO | IOBase):
cmdline.append('--sidecar')
cmdline.append('stream://sidecar')
parser.enable_api_mode()
options = parser.parse_args(cmdline)
@@ -219,6 +222,8 @@ def create_options(
options.input_file = input_file
if options.output_file == 'stream://output_file':
options.output_file = output_file
if options.sidecar == 'stream://sidecar':
options.sidecar = kwargs['sidecar']
return options
@@ -230,7 +235,7 @@ def ocr( # noqa: D417
language: Iterable[str] | None = None,
image_dpi: int | None = None,
output_type: str | None = None,
sidecar: StrPath | None = None,
sidecar: PathOrIO | None = None,
jobs: int | None = None,
use_threads: bool | None = None,
title: str | None = None,
@@ -343,7 +348,7 @@ def ocr( # noqa: D417
if not plugins:
plugins = []
elif isinstance(plugins, (str, Path)):
elif isinstance(plugins, str | Path):
plugins = [plugins]
else:
plugins = list(plugins)
+14 -11
View File
@@ -12,10 +12,10 @@ import queue
import signal
import sys
import threading
from collections.abc import Iterable
from collections.abc import Callable, Iterable
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed
from contextlib import suppress
from typing import Callable, Union
from typing import Union
from rich.console import Console as RichConsole
@@ -25,13 +25,13 @@ from ocrmypdf._progressbar import RichProgressBar
from ocrmypdf.exceptions import InputFileError
from ocrmypdf.helpers import remove_all_log_handlers
FuturesExecutorClass = Union[type[ThreadPoolExecutor], type[ProcessPoolExecutor]]
Queue = Union[multiprocessing.Queue, queue.Queue]
FuturesExecutorClass = Union[ # noqa: UP007
type[ThreadPoolExecutor], type[ProcessPoolExecutor]
]
Queue = Union[multiprocessing.Queue, queue.Queue] # noqa: UP007
UserInit = Callable[[], None]
WorkerInit = Callable[[Queue, UserInit, int], None]
RichTqdmProgressAdapter = RichProgressBar # Deprecated shim; remove in OCRmyPDF 16
def log_listener(q: Queue):
"""Listen to the worker processes and forward the messages to logging.
@@ -130,11 +130,14 @@ class StandardExecutor(Executor):
listener = threading.Thread(target=log_listener, args=(log_queue,))
listener.start()
with self.pbar_class(**progress_kwargs) as pbar, executor_class(
max_workers=max_workers,
initializer=initializer,
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
) as executor:
with (
self.pbar_class(**progress_kwargs) as pbar,
executor_class(
max_workers=max_workers,
initializer=initializer,
initargs=(log_queue, worker_initializer, logging.getLogger("").level),
) as executor,
):
futures = [executor.submit(task, *args) for args in task_arguments]
try:
for future in as_completed(futures):
@@ -8,7 +8,5 @@ from ocrmypdf import hookimpl
@hookimpl
def filter_pdf_page(
page, image_filename, output_pdf
): # pylint: disable=unused-argument
def filter_pdf_page(page, image_filename, output_pdf): # pylint: disable=unused-argument
return output_pdf
+16 -4
View File
@@ -6,6 +6,8 @@ from __future__ import annotations
import logging
from packaging.version import Version
from ocrmypdf import hookimpl
from ocrmypdf._exec import ghostscript
from ocrmypdf.exceptions import MissingDependencyError
@@ -15,7 +17,7 @@ log = logging.getLogger(__name__)
# Currently all blacklisted versions are lower than 9.55, so none need to
# be added here. If a future version is blacklisted, add it here.
BLACKLISTED_GS_VERSIONS: frozenset[str] = frozenset()
BLACKLISTED_GS_VERSIONS: frozenset[Version] = frozenset()
@hookimpl
@@ -52,14 +54,24 @@ def check_options(options):
program='gs',
package='ghostscript',
version_checker=ghostscript.version,
need_version='9.55', # Ubuntu 22.04's version
need_version='9.54', # RHEL 9's version; Ubuntu 22.04 has 9.55
)
gs_version = ghostscript.version()
if gs_version in BLACKLISTED_GS_VERSIONS:
raise MissingDependencyError(
f"Ghostscript {gs_version} contains serious regressions and is not "
"supported. Please upgrade to a newer version, or downgrade to the "
"previous version."
"supported. Please upgrade to a newer version."
)
if Version('10.0.0') <= gs_version < Version('10.02.1') and (
options.skip_text or options.redo_ocr
):
raise MissingDependencyError(
f"Ghostscript 10.0.0 through 10.02.0 (your version: {gs_version}) "
"contain serious regressions that corrupt PDFs with existing text, "
"such as those processed using --skip-text or --redo-ocr. "
"Please upgrade to a "
"newer version, or use --output-type pdf to avoid Ghostscript, or "
"use --force-ocr to discard existing text."
)
if options.output_type == 'pdfa':
@@ -2,9 +2,9 @@
# SPDX-License-Identifier: MPL-2.0
"""Built-in plugin to implement OCR using Tesseract."""
from __future__ import annotations
import argparse
import logging
import os
@@ -94,7 +94,8 @@ def add_options(parser):
)
tess.add_argument(
'--tesseract-downsample-large-images',
action='store_true',
action=argparse.BooleanOptionalAction,
default=True,
help=(
"Downsample large images before OCR. Tesseract has an upper limit on the "
"size images it will support. If this argument is given, OCRmyPDF will "
@@ -146,7 +147,11 @@ def check_options(options):
# Decide on what renderer to use
if options.pdf_renderer == 'auto':
options.pdf_renderer = 'sandwich'
if {'ara', 'heb', 'fas', 'per'} & set(options.languages):
log.info("Using sandwich renderer since there is an RTL language")
options.pdf_renderer = 'sandwich'
else:
options.pdf_renderer = 'hocr'
if not tesseract.has_thresholding() and options.tesseract_thresholding != 0:
log.warning(
@@ -216,7 +221,7 @@ class TesseractOcrEngine(OcrEngine):
@staticmethod
def creator_tag(options):
tag = '-PDF' if options.pdf_renderer == 'sandwich' else ''
tag = '-PDF' if options.pdf_renderer == 'sandwich' else '-hOCR'
return f"Tesseract OCR{tag} {TesseractOcrEngine.version()}"
def __str__(self):
+5 -4
View File
@@ -6,9 +6,10 @@
from __future__ import annotations
import argparse
from collections.abc import Mapping
from typing import Any, Callable, TypeVar
from collections.abc import Callable, Mapping
from typing import Any, TypeVar
from ocrmypdf._defaults import DEFAULT_ROTATE_PAGES_THRESHOLD
from ocrmypdf._version import PROGRAM_NAME as _PROGRAM_NAME
from ocrmypdf._version import __version__ as _VERSION
@@ -390,7 +391,7 @@ Online documentation is located at:
action='store',
type=numeric(float, 0),
metavar='MPixels',
help="Set maximum number of pixels to unpack before treating an image as a "
help="Set maximum number of megapixels to unpack before treating an image as a "
"decompression bomb",
default=250.0,
)
@@ -403,7 +404,7 @@ Online documentation is located at:
)
advanced.add_argument(
'--rotate-pages-threshold',
default=14.0,
default=DEFAULT_ROTATE_PAGES_THRESHOLD,
type=numeric(float, 0, 1000),
metavar='CONFIDENCE',
help="Only rotate pages when confidence is above this value (arbitrary "
Binary file not shown.
+1 -2
View File
@@ -20,13 +20,12 @@ from __future__ import annotations
import logging
import logging.handlers
import signal
from collections.abc import Iterable, Iterator
from collections.abc import Callable, Iterable, Iterator
from contextlib import suppress
from enum import Enum, auto
from itertools import islice, repeat, takewhile, zip_longest
from multiprocessing import Pipe, Process
from multiprocessing.connection import Connection, wait
from typing import Callable
from ocrmypdf import Executor, hookimpl
from ocrmypdf._concurrent import NullProgressBar
+3 -2
View File
@@ -10,7 +10,7 @@ import multiprocessing
import os
import shutil
import warnings
from collections.abc import Iterable, Sequence
from collections.abc import Callable, Iterable, Sequence
from contextlib import suppress
from decimal import Decimal
from io import StringIO
@@ -19,13 +19,13 @@ from pathlib import Path
from statistics import harmonic_mean
from typing import (
Any,
Callable,
Generic,
TypeVar,
)
import img2pdf
import pikepdf
from deprecation import deprecated
log = logging.getLogger(__name__)
@@ -136,6 +136,7 @@ class Resolution(Generic[T]):
return self._isclose(self.x, other.x) and self._isclose(self.y, other.y)
@deprecated(deprecated_in='15.4.0')
class NeverRaise(Exception):
"""An exception that is never raised."""
-461
View File
@@ -1,461 +0,0 @@
#!/usr/bin/env python3
# SPDX-FileCopyrightText: 2010 Jonathan Brinley
# SPDX-FileCopyrightText: 2013-2014 Julien Pfefferkorn
# SPDX-FileCopyrightText: 2022 James R. Barlow
# SPDX-License-Identifier: MIT
"""Transform .hocr and page image to text PDF."""
from __future__ import annotations
import argparse
import os
import re
import warnings
from math import atan, cos, sin
from pathlib import Path
from typing import Any, NamedTuple
from xml.etree import ElementTree
with warnings.catch_warnings():
# reportlab uses deprecated load_module
# shim can be removed when we require reportlab >= 3.7
warnings.filterwarnings(
'ignore', category=DeprecationWarning, message=r".*load_module.*"
)
from reportlab.lib.colors import black, cyan, magenta, red
from reportlab.lib.units import inch
from reportlab.pdfgen.canvas import Canvas
# According to Wikipedia these languages are supported in the ISO-8859-1 character
# set, meaning reportlab can generate them and they are compatible with hocr,
# assuming Tesseract has the necessary languages installed. Note that there may
# not be language packs for them.
HOCR_OK_LANGS = frozenset(
[
# Languages fully covered by Latin-1:
'afr', # Afrikaans
'alb', # Albanian
'ast', # Leonese
'baq', # Basque
'bre', # Breton
'cos', # Corsican
'eng', # English
'eus', # Basque
'fao', # Faoese
'gla', # Scottish Gaelic
'glg', # Galician
'glv', # Manx
'ice', # Icelandic
'ind', # Indonesian
'isl', # Icelandic
'ita', # Italian
'ltz', # Luxembourgish
'mal', # Malay Rumi
'mga', # Irish
'nor', # Norwegian
'oci', # Occitan
'por', # Portugeuse
'roh', # Romansh
'sco', # Scots
'sma', # Sami
'spa', # Spanish
'sqi', # Albanian
'swa', # Swahili
'swe', # Swedish
'tgl', # Tagalog
'wln', # Walloon
# Languages supported by Latin-1 except for a few rare characters that OCR
# is probably not trained to recognize anyway:
'cat', # Catalan
'cym', # Welsh
'dan', # Danish
'deu', # German
'dut', # Dutch
'est', # Estonian
'fin', # Finnish
'fra', # French
'hun', # Hungarian
'kur', # Kurdish
'nld', # Dutch
'wel', # Welsh
]
)
Element = ElementTree.Element
class Rect(NamedTuple):
"""A rectangle for managing PDF coordinates."""
x1: Any
y1: Any
x2: Any
y2: Any
class HocrTransformError(Exception):
"""Error while applying hOCR transform."""
class HocrTransform:
"""A class for converting documents from the hOCR format.
For details of the hOCR format, see:
http://kba.cloud/hocr-spec/.
"""
box_pattern = re.compile(r'bbox((\s+\d+){4})')
baseline_pattern = re.compile(
r'''
baseline \s+
([\-\+]?\d*\.?\d*) \s+ # +/- decimal float
([\-\+]?\d+) # +/- int''',
re.VERBOSE,
)
ligatures = str.maketrans(
{'': 'ff', '': 'ffi', '': 'ffl', '': 'fi', '': 'fl'}
)
def __init__(self, *, hocr_filename: str | Path, dpi: float):
"""Initialize the HocrTransform object."""
self.dpi = dpi
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
# if the hOCR file has a namespace, ElementTree requires its use to
# find elements
matches = re.match(r'({.*})html', self.hocr.getroot().tag)
self.xmlns = ''
if matches:
self.xmlns = matches.group(1)
# get dimension in pt (not pixel!!!!) of the OCRed image
self.width, self.height = None, None
for div in self.hocr.findall(self._child_xpath('div', 'ocr_page')):
coords = self.element_coordinates(div)
pt_coords = self.pt_from_pixel(coords)
self.width = pt_coords.x2 - pt_coords.x1
self.height = pt_coords.y2 - pt_coords.y1
# there shouldn't be more than one, and if there is, we don't want
# it
break
if self.width is None or self.height is None:
raise HocrTransformError("hocr file is missing page dimensions")
def __str__(self): # pragma: no cover
"""Return the textual content of the HTML body."""
if self.hocr is None:
return ''
body = self.hocr.find(self._child_xpath('body'))
if body:
return self._get_element_text(body)
else:
return ''
def _get_element_text(self, element: Element):
"""Return the textual content of the element and its children."""
text = ''
if element.text is not None:
text += element.text
for child in element:
text += self._get_element_text(child)
if element.tail is not None:
text += element.tail
return text
@classmethod
def element_coordinates(cls, element: Element) -> Rect:
"""Get coordinates of the bounding box around an element."""
out = Rect._make(0 for _ in range(4))
if 'title' in element.attrib:
matches = cls.box_pattern.search(element.attrib['title'])
if matches:
coords = matches.group(1).split()
out = Rect._make(int(coords[n]) for n in range(4))
return out
@classmethod
def baseline(cls, element: Element) -> tuple[float, float]:
"""Get baseline's slope and intercept."""
if 'title' in element.attrib:
matches = cls.baseline_pattern.search(element.attrib['title'])
if matches:
return float(matches.group(1)), int(matches.group(2))
return (0.0, 0.0)
def pt_from_pixel(self, pxl) -> Rect:
"""Returns the quantity in PDF units (pt) given quantity in pixels."""
return Rect._make((c / self.dpi * inch) for c in pxl)
def _child_xpath(self, html_tag: str, html_class: str | None = None) -> str:
xpath = f".//{self.xmlns}{html_tag}"
if html_class:
xpath += f"[@class='{html_class}']"
return xpath
@classmethod
def replace_unsupported_chars(cls, s: str) -> str:
"""Replaces characters with those available in the Helvetica typeface."""
return s.translate(cls.ligatures)
def to_pdf(
self,
*,
out_filename: Path,
image_filename: Path | None = None,
show_bounding_boxes: bool = False,
fontname: str = "Helvetica",
invisible_text: bool = False,
interword_spaces: bool = False,
) -> None:
"""Creates a PDF file with an image superimposed on top of the text.
Text is positioned according to the bounding box of the lines in
the hOCR file.
The image need not be identical to the image used to create the hOCR
file.
It can have a lower resolution, different color mode, etc.
Arguments:
out_filename: Path of PDF to write.
image_filename: Image to use for this file. If omitted, the OCR text
is shown.
show_bounding_boxes: Show bounding boxes around various text regions,
for debugging.
fontname: Name of font to use.
invisible_text: If True, text is rendered invisible so that is
selectable but never drawn. If False, text is visible and may
be seen if the image is skipped or deleted in Acrobat.
interword_spaces: If True, insert spaces between words rather than
drawing each word without spaces. Generally this improves text
extraction.
"""
# create the PDF file
# page size in points (1/72 in.)
pdf = Canvas(
os.fspath(out_filename),
pagesize=(self.width, self.height),
pageCompression=1,
)
# draw bounding box for each paragraph
# light blue for bounding box of paragraph
pdf.setStrokeColor(cyan)
# light blue for bounding box of paragraph
pdf.setFillColor(cyan)
pdf.setLineWidth(0) # no line for bounding box
for elem in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')):
elemtxt = self._get_element_text(elem).rstrip()
if len(elemtxt) == 0:
continue
pxl_coords = self.element_coordinates(elem)
pt = self.pt_from_pixel(pxl_coords) # pylint: disable=invalid-name
# draw the bbox border
if show_bounding_boxes: # pragma: no cover
pdf.rect(
pt.x1, self.height - pt.y2, pt.x2 - pt.x1, pt.y2 - pt.y1, fill=1
)
found_lines = False
for line in (
element
for element in self.hocr.iterfind(self._child_xpath('span'))
if 'class' in element.attrib
and element.attrib['class'] in {'ocr_header', 'ocr_line', 'ocr_textfloat'}
):
found_lines = True
self._do_line(
pdf,
line,
"ocrx_word",
fontname,
invisible_text,
interword_spaces,
show_bounding_boxes,
)
if not found_lines:
# Tesseract did not report any lines (just words)
root = self.hocr.find(self._child_xpath('div', 'ocr_page'))
self._do_line(
pdf,
root,
"ocrx_word",
fontname,
invisible_text,
interword_spaces,
show_bounding_boxes,
)
# put the image on the page, scaled to fill the page
if image_filename is not None:
pdf.drawImage(
os.fspath(image_filename), 0, 0, width=self.width, height=self.height
)
# finish up the page and save it
pdf.showPage()
pdf.save()
@classmethod
def polyval(cls, poly, x): # pragma: no cover
"""Calculate the value of a polynomial at a point."""
return x * poly[0] + poly[1]
def _do_line(
self,
pdf: Canvas,
line: Element | None,
elemclass: str,
fontname: str,
invisible_text: bool,
interword_spaces: bool,
show_bounding_boxes: bool,
):
if line is None:
return
pxl_line_coords = self.element_coordinates(line)
line_box = self.pt_from_pixel(pxl_line_coords)
line_height = line_box.y2 - line_box.y1
slope, pxl_intercept = self.baseline(line)
if abs(slope) < 0.005:
slope = 0.0
angle = atan(slope)
cos_a, sin_a = cos(angle), sin(angle)
text = pdf.beginText()
intercept = pxl_intercept / self.dpi * inch
# Don't allow the font to break out of the bounding box. Division by
# cos_a accounts for extra clearance between the glyph's vertical axis
# on a sloped baseline and the edge of the bounding box.
fontsize = (line_height - abs(intercept)) / cos_a
text.setFont(fontname, fontsize)
if invisible_text:
text.setTextRenderMode(3) # Invisible (indicates OCR text)
# Intercept is normally negative, so this places it above the bottom
# of the line box
baseline_y2 = self.height - (line_box.y2 + intercept)
if show_bounding_boxes: # pragma: no cover
# draw the baseline in magenta, dashed
pdf.setDash()
pdf.setStrokeColor(magenta)
pdf.setLineWidth(0.5)
# negate slope because it is defined as a rise/run in pixel
# coordinates and page coordinates have the y axis flipped
pdf.line(
line_box.x1,
baseline_y2,
line_box.x2,
self.polyval((-slope, baseline_y2), line_box.x2 - line_box.x1),
)
# light green for bounding box of word/line
pdf.setDash(6, 3)
pdf.setStrokeColor(red)
text.setTextTransform(cos_a, -sin_a, sin_a, cos_a, line_box.x1, baseline_y2)
pdf.setFillColor(black) # text in black
elements = line.findall(self._child_xpath('span', elemclass))
for elem in elements:
elemtxt = self._get_element_text(elem).strip()
elemtxt = self.replace_unsupported_chars(elemtxt)
if elemtxt == '':
continue
pxl_coords = self.element_coordinates(elem)
box = self.pt_from_pixel(pxl_coords)
if interword_spaces:
# if `--interword-spaces` is true, append a space
# to the end of each text element to allow simpler PDF viewers
# such as PDF.js to better recognize words in search and copy
# and paste. Do not remove space from last word in line, even
# though it would look better, because it will interfere with
# naive text extraction. \n does not work either.
elemtxt += ' '
box = Rect._make(
(
box.x1,
line_box.y1,
box.x2 + pdf.stringWidth(' ', fontname, line_height),
line_box.y2,
)
)
box_width = box.x2 - box.x1
font_width = pdf.stringWidth(elemtxt, fontname, fontsize)
# draw the bbox border
if show_bounding_boxes: # pragma: no cover
pdf.rect(
box.x1, self.height - line_box.y2, box_width, line_height, fill=0
)
# Adjust relative position of cursor
# This is equivalent to:
# text.setTextOrigin(pt.x1, self.height - line_box.y2)
# but the former generates a full text reposition matrix (Tm) in the
# content stream while this issues a "offset" (Td) command.
# .moveCursor() is relative to start of the text line, where the
# "text line" means whatever reportlab defines it as. Do not use
# use .getCursor(), since moveCursor() rather unintuitively plans
# its moves relative to .getStartOfLine().
# For skewed lines, in the text transform we set up a rotated
# coordinate system, so we don't have to account for the
# incremental offset. Surprisingly most PDF viewers can handle this.
cursor = text.getStartOfLine()
dx = box.x1 - cursor[0]
dy = baseline_y2 - cursor[1]
text.moveCursor(dx, dy)
# If reportlab tells us this word is 0 units wide, our best seems
# to be to suppress this text
if font_width > 0:
text.setHorizScale(100 * box_width / font_width)
text.textOut(elemtxt)
pdf.drawText(text)
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
parser.add_argument(
'-b',
'--boundingboxes',
action="store_true",
default=False,
help='Show bounding boxes borders',
)
parser.add_argument(
'-r',
'--resolution',
type=int,
default=300,
help='Resolution of the image that was OCRed',
)
parser.add_argument(
'-i',
'--image',
default=None,
help='Path to the image to be placed above the text',
)
parser.add_argument(
'--interword-spaces',
action='store_true',
default=False,
help='Add spaces between words',
)
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
parser.add_argument('outputfile', help='Path to the PDF file to be generated')
args = parser.parse_args()
hocr = HocrTransform(hocr_filename=args.hocrfile, dpi=args.resolution)
hocr.to_pdf(
out_filename=args.outputfile,
image_filename=args.image,
show_bounding_boxes=args.boundingboxes,
interword_spaces=args.interword_spaces,
)
+18
View File
@@ -0,0 +1,18 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MIT
"""Transform .hocr and page image to text PDF."""
from __future__ import annotations
from ocrmypdf.hocrtransform._hocr import (
DebugRenderOptions,
HocrTransform,
HocrTransformError,
)
__all__ = (
'HocrTransform',
'HocrTransformError',
'DebugRenderOptions',
)
+40
View File
@@ -0,0 +1,40 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MIT
"""Simple CLI for testing HOCR."""
import argparse
from ocrmypdf.hocrtransform import HocrTransform
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Convert hocr file to PDF')
parser.add_argument(
'-b',
'--boundingboxes',
action="store_true",
default=False,
help='Show bounding boxes borders',
)
parser.add_argument(
'-r',
'--resolution',
type=int,
default=300,
help='Resolution of the image that was OCRed',
)
parser.add_argument(
'-i',
'--image',
default=None,
help='Path to the image to be placed above the text',
)
parser.add_argument('hocrfile', help='Path to the hocr file to be parsed')
parser.add_argument('outputfile', help='Path to the PDF file to be generated')
args = parser.parse_args()
hocr = HocrTransform(hocr_filename=args.hocrfile, dpi=args.resolution)
hocr.to_pdf(
out_filename=args.outputfile,
image_filename=args.image,
)
+141
View File
@@ -0,0 +1,141 @@
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MPL-2.0
from __future__ import annotations
import logging
import unicodedata
import zlib
from importlib.resources import files as package_files
from pikepdf import (
Dictionary,
Name,
Pdf,
)
from pikepdf.canvas import Font
log = logging.getLogger(__name__)
class EncodableFont(Font):
def text_encode(self, text: str) -> bytes:
raise NotImplementedError()
class GlyphlessFont(EncodableFont):
CID_TO_GID_DATA = zlib.compress(b"\x00\x01" * 65536)
GLYPHLESS_FONT_NAME = 'pdf.ttf'
GLYPHLESS_FONT = (package_files('ocrmypdf.data') / GLYPHLESS_FONT_NAME).read_bytes()
CHAR_ASPECT = 2
def __init__(self):
pass
def text_width(self, text: str, fontsize: float) -> float:
"""Estimate the width of a text string when rendered with the given font."""
# NFKC: split ligatures, combine diacritics
return len(unicodedata.normalize("NFKC", text)) * (fontsize / self.CHAR_ASPECT)
def text_encode(self, text: str) -> bytes:
return text.encode('utf-16be')
def register(self, pdf: Pdf):
"""Register the glyphless font.
Create several data structures in the Pdf to describe the font. While it create
the data, a reference should be set in at least one page's /Resources dictionary
to retain the font in the output PDF and ensure it is usable on that page.
"""
PLACEHOLDER = Name.Placeholder
basefont = pdf.make_indirect(
Dictionary(
BaseFont=Name.GlyphLessFont,
DescendantFonts=[PLACEHOLDER],
Encoding=Name("/Identity-H"),
Subtype=Name.Type0,
ToUnicode=PLACEHOLDER,
Type=Name.Font,
)
)
cid_font_type2 = pdf.make_indirect(
Dictionary(
BaseFont=Name.GlyphLessFont,
CIDToGIDMap=PLACEHOLDER,
CIDSystemInfo=Dictionary(
Ordering="Identity",
Registry="Adobe",
Supplement=0,
),
FontDescriptor=PLACEHOLDER,
Subtype=Name.CIDFontType2,
Type=Name.Font,
DW=1000 // self.CHAR_ASPECT,
)
)
basefont.DescendantFonts = [cid_font_type2]
cid_font_type2.CIDToGIDMap = pdf.make_stream(
self.CID_TO_GID_DATA, Filter=Name.FlateDecode
)
basefont.ToUnicode = pdf.make_stream(
b"/CIDInit /ProcSet findresource begin\n"
b"12 dict begin\n"
b"begincmap\n"
b"/CIDSystemInfo\n"
b"<<\n"
b" /Registry (Adobe)\n"
b" /Ordering (UCS)\n"
b" /Supplement 0\n"
b">> def\n"
b"/CMapName /Adobe-Identify-UCS def\n"
b"/CMapType 2 def\n"
b"1 begincodespacerange\n"
b"<0000> <FFFF>\n"
b"endcodespacerange\n"
b"1 beginbfrange\n"
b"<0000> <FFFF> <0000>\n"
b"endbfrange\n"
b"endcmap\n"
b"CMapName currentdict /CMap defineresource pop\n"
b"end\n"
b"end\n"
)
font_descriptor = pdf.make_indirect(
Dictionary(
Ascent=1000,
CapHeight=1000,
Descent=-1,
Flags=5, # Fixed pitch and symbolic
FontBBox=[0, 0, 1000 // self.CHAR_ASPECT, 1000],
FontFile2=PLACEHOLDER,
FontName=Name.GlyphLessFont,
ItalicAngle=0,
StemV=80,
Type=Name.FontDescriptor,
)
)
font_descriptor.FontFile2 = pdf.make_stream(self.GLYPHLESS_FONT)
cid_font_type2.FontDescriptor = font_descriptor
return basefont
class Courier(EncodableFont):
"""Courier font."""
def text_width(self, text: str, fontsize: float) -> float:
"""Estimate the width of a text string when rendered with the given font."""
return len(text) * fontsize
def text_encode(self, text: str) -> bytes:
return text.encode('pdfdoc', errors='ignore')
def register(self, pdf: Pdf) -> Dictionary:
"""Register the font."""
return pdf.make_indirect(
Dictionary(
BaseFont=Name.Courier,
Type=Name.Font,
Subtype=Name.Type1,
)
)
+488
View File
@@ -0,0 +1,488 @@
# SPDX-FileCopyrightText: 2010 Jonathan Brinley
# SPDX-FileCopyrightText: 2013-2014 Julien Pfefferkorn
# SPDX-FileCopyrightText: 2023 James R. Barlow
# SPDX-License-Identifier: MIT
"""hOCR transform implementation."""
from __future__ import annotations
import logging
import os
import re
import unicodedata
from dataclasses import dataclass
from itertools import pairwise
from math import atan, cos, pi
from pathlib import Path
from xml.etree import ElementTree
from pikepdf import Matrix, Name, Rectangle
from pikepdf.canvas import (
BLACK,
BLUE,
CYAN,
DARKGREEN,
GREEN,
MAGENTA,
RED,
Canvas,
Text,
TextDirection,
)
from ocrmypdf.hocrtransform._font import EncodableFont as Font
from ocrmypdf.hocrtransform._font import GlyphlessFont
log = logging.getLogger(__name__)
INCH = 72.0
Element = ElementTree.Element
@dataclass
class DebugRenderOptions:
"""A class for managing rendering options."""
render_paragraph_bbox: bool = False
render_baseline: bool = False
render_triangle: bool = False
render_line_bbox: bool = False
render_word_bbox: bool = False
render_space_bbox: bool = False
class HocrTransformError(Exception):
"""Error while applying hOCR transform."""
class HocrTransform:
"""A class for converting documents from the hOCR format.
For details of the hOCR format, see:
http://kba.cloud/hocr-spec/.
"""
box_pattern = re.compile(r'bbox (\d+) (\d+) (\d+) (\d+)')
baseline_pattern = re.compile(
r'''
baseline \s+
([\-\+]?\d*\.?\d*) \s+ # +/- decimal float
([\-\+]?\d+) # +/- int''',
re.VERBOSE,
)
def __init__(
self,
*,
hocr_filename: str | Path,
dpi: float,
debug: bool = False,
fontname: Name = Name("/f-0-0"),
font: Font = GlyphlessFont(),
debug_render_options: DebugRenderOptions | None = None,
):
"""Initialize the HocrTransform object."""
if debug:
log.warning("Use debug_render_options instead", DeprecationWarning)
self.render_options = DebugRenderOptions(
render_baseline=debug,
render_triangle=debug,
render_line_bbox=False,
render_word_bbox=debug,
render_paragraph_bbox=False,
render_space_bbox=False,
)
else:
self.render_options = debug_render_options or DebugRenderOptions()
self.dpi = dpi
self.hocr = ElementTree.parse(os.fspath(hocr_filename))
self._fontname = fontname
self._font = font
# if the hOCR file has a namespace, ElementTree requires its use to
# find elements
matches = re.match(r'({.*})html', self.hocr.getroot().tag)
self.xmlns = ''
if matches:
self.xmlns = matches.group(1)
for div in self.hocr.findall(self._child_xpath('div', 'ocr_page')):
coords = self.element_coordinates(div)
if not coords:
raise HocrTransformError("hocr file is missing page dimensions")
self.width = (coords.urx - coords.llx) / (self.dpi / INCH)
self.height = (coords.ury - coords.lly) / (self.dpi / INCH)
# Stop after first div that has page coordinates
break
def _get_element_text(self, element: Element) -> str:
"""Return the textual content of the element and its children."""
text = element.text if element.text is not None else ''
for child in element:
text += self._get_element_text(child)
text += element.tail if element.tail is not None else ''
return text
@classmethod
def element_coordinates(cls, element: Element) -> Rectangle | None:
"""Get coordinates of the bounding box around an element."""
matches = cls.box_pattern.search(element.attrib.get('title', ''))
if not matches:
return None
return Rectangle(
float(matches.group(1)), # llx = left
float(matches.group(2)), # lly = top
float(matches.group(3)), # urx = right
float(matches.group(4)), # ury = bottom
)
@classmethod
def baseline(cls, element: Element) -> tuple[float, float]:
"""Get baseline's slope and intercept."""
matches = cls.baseline_pattern.search(element.attrib.get('title', ''))
if not matches:
return (0.0, 0.0)
return float(matches.group(1)), int(matches.group(2))
def _child_xpath(self, html_tag: str, html_class: str | None = None) -> str:
xpath = f".//{self.xmlns}{html_tag}"
if html_class:
xpath += f"[@class='{html_class}']"
return xpath
@classmethod
def normalize_text(cls, s: str) -> str:
"""Normalize the given text using the NFKC normalization form."""
return unicodedata.normalize("NFKC", s)
def to_pdf(
self,
*,
out_filename: Path,
image_filename: Path | None = None,
invisible_text: bool = True,
) -> None:
"""Creates a PDF file with an image superimposed on top of the text.
Text is positioned according to the bounding box of the lines in
the hOCR file.
The image need not be identical to the image used to create the hOCR
file.
It can have a lower resolution, different color mode, etc.
Arguments:
out_filename: Path of PDF to write.
image_filename: Image to use for this file. If omitted, the OCR text
is shown.
invisible_text: If True, text is rendered invisible so that is
selectable but never drawn. If False, text is visible and may
be seen if the image is skipped or deleted in Acrobat.
"""
# create the PDF file
# page size in points (1/72 in.)
canvas = Canvas(page_size=(self.width, self.height))
canvas.add_font(self._fontname, self._font)
page_matrix = (
Matrix()
.translated(0, self.height)
.scaled(1, -1)
.scaled(INCH / self.dpi, INCH / self.dpi)
)
log.debug(page_matrix)
with canvas.do.save_state(cm=page_matrix):
self._debug_draw_paragraph_boxes(canvas)
found_lines = False
for par in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')):
for line in (
element
for element in par.iterfind(self._child_xpath('span'))
if 'class' in element.attrib
and element.attrib['class']
in {'ocr_header', 'ocr_line', 'ocr_textfloat'}
):
found_lines = True
direction = self._get_text_direction(par)
inject_word_breaks = self._get_inject_word_breaks(par)
self._do_line(
canvas,
line,
"ocrx_word",
invisible_text,
direction,
inject_word_breaks,
)
if not found_lines:
# Tesseract did not report any lines (just words)
root = self.hocr.find(self._child_xpath('div', 'ocr_page'))
direction = self._get_text_direction(root)
self._do_line(
canvas,
root,
"ocrx_word",
invisible_text,
direction,
True,
)
# put the image on the page, scaled to fill the page
if image_filename is not None:
canvas.do.draw_image(
image_filename, 0, 0, width=self.width, height=self.height
)
# finish up the page and save it
canvas.to_pdf().save(out_filename)
def _get_text_direction(self, par):
"""Get the text direction of the paragraph.
Arabic, Hebrew, Persian, are right-to-left languages.
"""
return (
TextDirection.RTL
if par.attrib.get('dir', 'ltr') == 'rtl'
else TextDirection.LTR
)
def _get_inject_word_breaks(self, par):
"""Determine whether word breaks should be injected.
In Chinese, Japanese, and Korean, word breaks are not injected, because
words are usually one or two characters and separators are usually explicit.
In all other languages, we inject word breaks to help word segmentation.
"""
lang = par.attrib.get('lang', '')
log.debug(lang)
if lang in {'chi_sim', 'chi_tra', 'jpn', 'kor'}:
return False
return True
@classmethod
def polyval(cls, poly, x): # pragma: no cover
"""Calculate the value of a polynomial at a point."""
return x * poly[0] + poly[1]
def _do_line(
self,
canvas: Canvas,
line: Element | None,
elemclass: str,
invisible_text: bool,
text_direction: TextDirection,
inject_word_breaks: bool,
):
"""Render the text for a given line.
The canvas's coordinate system must be configured so that hOCR pixel
coordinates are mapped to PDF coordinates.
"""
if line is None:
return
line_box = self.element_coordinates(line)
if not line_box:
return
if line_box.ury <= line_box.lly:
log.error("line box is invalid so we cannot render it: box=%s text=%s",
line_box, self._get_element_text(line))
return
self._debug_draw_line_bbox(canvas, line_box)
# Baseline is a polynomial (usually straight line) that describes the
# text baseline relative to the bottom left corner of the line bounding
# box.
bottom_left_corner = line_box.llx, line_box.ury
slope, intercept = self.baseline(line)
if abs(slope) < 0.005:
slope = 0.0
angle = atan(slope)
# Setup a new coordinate system on the line box's intercept and rotated by
# its slope.
line_matrix = (
Matrix()
.translated(*bottom_left_corner)
.translated(0, intercept)
.rotated(angle / pi * 180)
)
log.debug(line_matrix)
with canvas.do.save_state(cm=line_matrix):
text = Text(direction=text_direction)
# Don't allow the font to break out of the bounding box. Division by
# cos_a accounts for extra clearance between the glyph's vertical axis
# on a sloped baseline and the edge of the bounding box.
line_box_height = abs(line_box.height) / cos(angle)
fontsize = line_box_height + intercept
text.font(self._fontname, fontsize)
text.render_mode(3 if invisible_text else 0)
self._debug_draw_baseline(
canvas, line_matrix.inverse().transform(line_box), 0
)
canvas.do.fill_color(BLACK) # text in black
elements = line.findall(self._child_xpath('span', elemclass))
for elem, next_elem in pairwise(elements + [None]):
self._do_line_word(
canvas,
line_matrix,
text,
fontsize,
elem,
next_elem,
text_direction,
inject_word_breaks,
)
canvas.do.draw_text(text)
def _do_line_word(
self,
canvas: Canvas,
line_matrix: Matrix,
text: Text,
fontsize: float,
elem: Element,
next_elem: Element | None,
text_direction: TextDirection,
inject_word_breaks: bool,
):
"""Render the text for a single word."""
if elem is None:
return
elemtxt = self.normalize_text(self._get_element_text(elem).strip())
if elemtxt == '':
return
hocr_box = self.element_coordinates(elem)
if hocr_box is None:
return
box = line_matrix.inverse().transform(hocr_box)
font_width = self._font.text_width(elemtxt, fontsize)
# Debug sketches
self._debug_draw_word_triangle(canvas, box)
self._debug_draw_word_bbox(canvas, box)
# If this word is 0 units wide, our best bet seems to be to suppress this text
if text_direction == TextDirection.RTL:
log.info("RTL: %s", elemtxt)
if font_width > 0:
if text_direction == TextDirection.LTR:
text.text_transform(Matrix(1, 0, 0, -1, box.llx, 0))
elif text_direction == TextDirection.RTL:
text.text_transform(Matrix(-1, 0, 0, -1, box.llx + box.width, 0))
text.horiz_scale(100 * box.width / font_width)
text.show(self._font.text_encode(elemtxt))
# Get coordinates of the next word (if there is one)
hocr_next_box = (
self.element_coordinates(next_elem) if next_elem is not None else None
)
if hocr_next_box is None:
return
# Render a space this word and the next word. The explicit space helps
# PDF viewers identify the word break, and horizontally scaling it to
# occupy the space the between the words helps the PDF viewer
# avoid combiningthewordstogether.
if not inject_word_breaks:
return
next_box = line_matrix.inverse().transform(hocr_next_box)
if text_direction == TextDirection.LTR:
space_box = Rectangle(box.urx, box.lly, next_box.llx, next_box.ury)
elif text_direction == TextDirection.RTL:
space_box = Rectangle(next_box.urx, box.lly, box.llx, next_box.ury)
self._debug_draw_space_bbox(canvas, space_box)
space_width = self._font.text_width(' ', fontsize)
if space_width > 0:
if text_direction == TextDirection.LTR:
text.text_transform(Matrix(1, 0, 0, -1, space_box.llx, 0))
elif text_direction == TextDirection.RTL:
text.text_transform(
Matrix(-1, 0, 0, -1, space_box.llx + space_box.width, 0)
)
text.horiz_scale(100 * space_box.width / space_width)
text.show(self._font.text_encode(' '))
def _debug_draw_paragraph_boxes(self, canvas: Canvas, color=CYAN):
"""Draw boxes around paragraphs in the document."""
if not self.render_options.render_paragraph_bbox: # pragma: no cover
return
with canvas.do.save_state():
# draw box around paragraph
canvas.do.stroke_color(color).line_width(0.1)
for elem in self.hocr.iterfind(self._child_xpath('p', 'ocr_par')):
elemtxt = self._get_element_text(elem).strip()
if len(elemtxt) == 0:
continue
ocr_par = self.element_coordinates(elem)
if ocr_par is None:
continue
canvas.do.rect(
ocr_par.llx, ocr_par.lly, ocr_par.width, ocr_par.height, fill=0
)
def _debug_draw_line_bbox(self, canvas: Canvas, line_box: Rectangle, color=BLUE):
"""Render the bounding box of a text line."""
if not self.render_options.render_line_bbox: # pragma: no cover
return
with canvas.do.save_state():
canvas.do.stroke_color(color).line_width(0.15).rect(
line_box.llx, line_box.lly, line_box.width, line_box.height, fill=0
)
def _debug_draw_word_triangle(
self, canvas: Canvas, box: Rectangle, color=RED, line_width=0.1
):
"""Render a triangle that conveys word height and drawing direction."""
if not self.render_options.render_triangle: # pragma: no cover
return
with canvas.do.save_state():
canvas.do.stroke_color(color).line_width(line_width).line(
box.llx, box.lly, box.urx, box.lly
).line(box.urx, box.lly, box.llx, box.ury).line(
box.llx, box.lly, box.llx, box.ury
)
def _debug_draw_word_bbox(
self, canvas: Canvas, box: Rectangle, color=GREEN, line_width=0.1
):
"""Render a box depicting the word."""
if not self.render_options.render_word_bbox: # pragma: no cover
return
with canvas.do.save_state():
canvas.do.stroke_color(color).line_width(line_width).rect(
box.llx, box.lly, box.width, box.height, fill=0
)
def _debug_draw_space_bbox(
self, canvas: Canvas, box: Rectangle, color=DARKGREEN, line_width=0.1
):
"""Render a box depicting the space between two words."""
if not self.render_options.render_space_bbox: # pragma: no cover
return
with canvas.do.save_state():
canvas.do.fill_color(color).line_width(line_width).rect(
box.llx, box.lly, box.width, box.height, fill=1
)
def _debug_draw_baseline(
self,
canvas: Canvas,
line_box: Rectangle,
baseline_lly,
color=MAGENTA,
line_width=0.25,
):
"""Render the text baseline."""
if not self.render_options.render_baseline:
return
with canvas.do.save_state():
canvas.do.stroke_color(color).line_width(line_width).line(
line_box.llx,
baseline_lly,
line_box.urx,
baseline_lly,
)
+6 -14
View File
@@ -7,17 +7,9 @@ from __future__ import annotations
import logging
from math import floor, sqrt
from typing import Optional
from PIL import Image
# While from __future__ import annotations, we use singledispatch here, which
# does not support annotations. Disable check about using old-style typing
# until Python 3.10, OR when drop singledispatch in ocrmypdf 15.
# ruff: noqa: UP006
# ruff: noqa: UP007
log = logging.getLogger(__name__)
@@ -38,9 +30,9 @@ def _calculate_downsample(
image_size: tuple[int, int],
bytes_per_pixel: int,
*,
max_size: Optional[tuple[int, int]] = None,
max_pixels: Optional[int] = None,
max_bytes: Optional[int] = None,
max_size: tuple[int, int] | None = None,
max_pixels: int | None = None,
max_bytes: int | None = None,
) -> tuple[int, int]:
"""Calculate image size required to downsample an image to fit limits.
@@ -98,9 +90,9 @@ def _calculate_downsample(
def calculate_downsample(
image: Image.Image,
*,
max_size: Optional[tuple[int, int]] = None,
max_pixels: Optional[int] = None,
max_bytes: Optional[int] = None,
max_size: tuple[int, int] | None = None,
max_pixels: int | None = None,
max_bytes: int | None = None,
) -> tuple[int, int]:
"""Calculate image size required to downsample an image to fit limits.
+5 -3
View File
@@ -7,12 +7,12 @@ Derived from
https://www.loc.gov/standards/iso639-2/ascii_8bits.html
"""
from typing import NamedTuple
class ISOCodeData(NamedTuple):
"""Data for a single ISO 639 code."""
alt: str
alpha_2: str
english: str
@@ -168,8 +168,10 @@ ISO_639_3 = {
'chu': ISOCodeData(
'',
'cu',
('Church Slavic; Old Slavonic; Church Slavonic;'
' Old Bulgarian; Old Church Slavonic'),
(
'Church Slavic; Old Slavonic; Church Slavonic;'
' Old Bulgarian; Old Church Slavonic'
),
"slavon d'église; vieux slave; slavon liturgique; vieux bulgare",
),
'chv': ISOCodeData('', 'cv', 'Chuvash', 'tchouvache'),
+12 -12
View File
@@ -3,7 +3,6 @@
"""Post-processing image optimization of OCR PDFs."""
from __future__ import annotations
import logging
@@ -11,11 +10,10 @@ import sys
import tempfile
import threading
from collections import defaultdict
from collections.abc import Iterator, MutableSet, Sequence
from collections.abc import Callable, Iterator, MutableSet, Sequence
from os import fspath
from pathlib import Path
from typing import Any, Callable, NamedTuple, NewType
from warnings import warn
from typing import Any, NamedTuple, NewType
from zlib import compress
import img2pdf
@@ -71,15 +69,9 @@ def jpg_name(root: Path, xref: Xref) -> Path:
def extract_image_filter(
image: Stream, xref: Xref, *args
image: Stream, xref: Xref
) -> tuple[PdfImage, tuple[Name, Object]] | None:
"""Determine if an image is extractable."""
if isinstance(image, Pdf):
# Support deprecated old function signature
# TODO Remove for v16 and drop *args from current function signature
image, xref = args[0], args[1]
warn("extract_image_filter: pdf, root parameters ignored", DeprecationWarning)
if image.Subtype != Name.Image:
return None
if image.Length < 100:
@@ -97,6 +89,7 @@ def extract_image_filter(
if (
len(pim.filter_decodeparms) == 2
and first_filtdp[0] == Name.FlateDecode
and first_filtdp[1] is not None
and first_filtdp[1].get(Name.Predictor, 1) == 1
and second_filtdp[0] == Name.DCTDecode
and not second_filtdp[1]
@@ -161,6 +154,13 @@ def extract_image_jbig2(
with imgname.open('wb') as f:
ext = pim.extract_to(stream=f)
imgname.rename(imgname.with_suffix(ext))
except NotImplementedError as e:
if '/Decode' in str(e):
log.debug(
f"xref {xref}: skipping image with unsupported Decode table"
)
return None
raise
except UnsupportedImageTypeError:
return None
finally:
@@ -376,7 +376,7 @@ def _produce_jbig2_images(
options.jbig2_threshold,
)
def jbig2_single_args(root, groups: dict[int, list[XrefExt]]):
def jbig2_single_args(root: Path, groups: dict[int, list[XrefExt]]):
for group, xref_exts in groups.items():
prefix = f'group{group:08d}'
# Second loop is to ensure multiple images per page are unpacked
+1 -1
View File
@@ -90,7 +90,7 @@ def generate_pdfa_ps(target_filename: Path, icc: str = 'sRGB'):
icc: ICC identifier such as 'sRGB'
References:
Adobe PDFMARK Reference:
https://www.adobe.com/content/dam/acom/en/devnet/acrobat/pdfs/pdfmark_reference.pdf
https://opensource.adobe.com/dc-acrobat-sdk-docs/library/pdfmark/
"""
if icc != 'sRGB':
raise NotImplementedError("Only supporting sRGB")
+37 -16
View File
@@ -10,9 +10,8 @@ import atexit
import logging
import re
import statistics
import sys
from collections import defaultdict
from collections.abc import Container, Iterable, Iterator, Mapping, Sequence
from collections.abc import Callable, Container, Iterable, Iterator, Mapping, Sequence
from contextlib import contextmanager
from decimal import Decimal
from enum import Enum, auto
@@ -20,18 +19,18 @@ from functools import partial
from math import hypot, inf, isclose
from os import PathLike
from pathlib import Path
from typing import Callable, NamedTuple
from typing import NamedTuple
from warnings import warn
from pdfminer.layout import LTPage, LTTextBox
from pikepdf import (
Matrix,
Name,
Object,
Page,
Pdf,
PdfImage,
PdfInlineImage,
PdfMatrix,
Stream,
UnsupportedImageTypeError,
parse_content_stream,
@@ -209,7 +208,7 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
CTM unchanged.
"""
stack = []
ctm = PdfMatrix(initial_shorthand)
ctm = Matrix(initial_shorthand)
xobject_settings: list[XobjectSettings] = []
inline_images: list[InlineSettings] = []
name_index = defaultdict(lambda: [])
@@ -240,7 +239,13 @@ def _interpret_contents(contentstream: Object, initial_shorthand=UNIT_SQUARE):
# to do. Just pretend nothing happened, keep calm and carry on.
warn("PDF graphics stack underflowed - PDF may be malformed")
elif operator == 'cm':
ctm = PdfMatrix(operands) @ ctm
try:
ctm = Matrix(operands) @ ctm
except ValueError:
raise InputFileError(
"PDF content stream is corrupt - this PDF is malformed. "
"Use a PDF editor that is capable of visually inspecting the PDF."
)
elif operator == 'Do':
image_name = operands[0]
settings = XobjectSettings(
@@ -614,12 +619,12 @@ def _process_content_streams(
):
# Set the CTM to the state it was when the "Do" operator was
# encountered that is drawing this instance of the Form XObject
ctm = PdfMatrix(shorthand) if shorthand else PdfMatrix.identity()
ctm = Matrix(shorthand) if shorthand else Matrix()
# A Form XObject may provide its own matrix to map form space into
# user space. Get this if one exists
form_shorthand = container.get(Name.Matrix, PdfMatrix.identity())
form_matrix = PdfMatrix(form_shorthand)
form_shorthand = container.get(Name.Matrix, Matrix())
form_matrix = Matrix(form_shorthand)
# Concatenate form matrix with CTM to ensure CTM is correct for
# drawing this instance of the XObject
@@ -854,6 +859,12 @@ class PageInfo:
width_pt = mediabox[2] - mediabox[0]
height_pt = mediabox[3] - mediabox[1]
# self._artbox = [float(d) for d in page.artbox.as_list()]
# self._bleedbox = [float(d) for d in page.bleedbox.as_list()]
self._cropbox = [float(d) for d in page.cropbox.as_list()]
self._mediabox = [float(d) for d in page.mediabox.as_list()]
self._trimbox = [float(d) for d in page.trimbox.as_list()]
check_this_page = pageno in check_pages
if check_this_page and detailed_analysis:
@@ -970,6 +981,21 @@ class PageInfo:
else:
raise ValueError("rotation must be a cardinal angle")
@property
def cropbox(self) -> FloatRect:
"""Return cropbox of page in PDF coordinates."""
return self._cropbox
@property
def mediabox(self) -> FloatRect:
"""Return mediabox of page in PDF coordinates."""
return self._mediabox
@property
def trimbox(self) -> FloatRect:
"""Return trimbox of page in PDF coordinates."""
return self._trimbox
@property
def images(self) -> list[ImageInfo]:
"""Return images."""
@@ -1039,12 +1065,7 @@ class PageInfo:
weights = [area / total_drawn_area for area in image_areas]
# Calculate harmonic mean of DPIs weighted by area
if sys.version_info >= (3, 10):
weighted_dpi = statistics.harmonic_mean(image_dpis, weights)
else:
weighted_dpi = sum(weights) / sum(
weight / dpi for weight, dpi in zip(weights, image_dpis)
)
weighted_dpi = statistics.harmonic_mean(image_dpis, weights)
max_dpi = max(image_dpis)
dpi_average_max_ratio = weighted_dpi / max_dpi
@@ -1155,7 +1176,7 @@ class PdfInfo:
@property
def filename(self) -> str | Path:
"""Return filename of PDF."""
if not isinstance(self._infile, (str, Path)):
if not isinstance(self._infile, str | Path):
raise NotImplementedError("can't get filename from stream")
return self._infile
+2 -2
View File
@@ -5,12 +5,12 @@
from __future__ import annotations
import re
from collections.abc import Mapping
from collections.abc import Iterator, Mapping
from contextlib import contextmanager
from math import copysign
from os import PathLike
from pathlib import Path
from typing import Any, Iterator
from typing import Any
from unittest.mock import patch
import pdfminer
-1
View File
@@ -3,7 +3,6 @@
"""Utilities to measure OCR quality."""
from __future__ import annotations
import re
+3 -3
View File
@@ -8,12 +8,11 @@ import logging
import os
import re
import sys
from collections.abc import Mapping, Sequence
from collections.abc import Callable, Mapping, Sequence
from contextlib import suppress
from pathlib import Path
from subprocess import PIPE, STDOUT, CalledProcessError, CompletedProcess, Popen
from subprocess import run as subprocess_run
from typing import Callable, Union
from packaging.version import Version
@@ -23,7 +22,7 @@ from ocrmypdf.exceptions import MissingDependencyError
log = logging.getLogger(__name__)
Args = Sequence[Union[Path, str]]
Args = Sequence[Path | str]
OsEnviron = os._Environ # pylint: disable=protected-access
@@ -172,6 +171,7 @@ def get_version(
) from e
except CalledProcessError as e:
if e.returncode != 0:
log.exception(e)
raise MissingDependencyError(
f"Ran program '{program}' but it exited with an error:\n{e.output}"
) from e
+3 -8
View File
@@ -9,18 +9,13 @@ import os
import re
import shutil
import sys
from collections.abc import Iterable, Iterator
from collections.abc import Callable, Iterable, Iterator
from itertools import chain
from pathlib import Path
from typing import Any, Callable, TypeVar
from typing import Any, TypeAlias, TypeVar
from packaging.version import InvalidVersion, Version
if sys.version_info >= (3, 10):
from typing import TypeAlias
else:
from typing_extensions import TypeAlias # pragma: no cover
if sys.platform == 'win32':
# mypy understands 'if sys.platform' better than try/except ModuleNotFoundError
import winreg # pylint: disable=import-error
@@ -84,7 +79,7 @@ def registry_path_ghostscript(env=None) -> Iterator[Path]:
registry_subkeys(k), key=ghostscript_version_key, default=(0, 0, 0)
)
with winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, fr"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
winreg.HKEY_LOCAL_MACHINE, rf"SOFTWARE\Artifex\GPL Ghostscript\{latest_gs}"
) as k:
for _, gs_path, _ in registry_values(k):
yield Path(gs_path) / 'bin'
@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.3.2' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.jcdzf4up/000001_ocr.png"; bbox 0 0 4000 2864; ppageno 0; scan_res 2400 2400'>
<div class='ocr_carea' id='block_1_1' title="bbox 251 146 2173 237">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 251 146 2173 237">
<span class='ocr_line' id='line_1_1' title="bbox 251 146 2173 237; baseline -0.006 5; x_size 99.418808; x_descenders 9.4188042; x_ascenders 32">
<span class='ocrx_word' id='word_1_1' title='bbox 251 154 274 176; x_wconf 89'>i</span>
<span class='ocrx_word' id='word_1_2' title='bbox 1080 146 1152 237; x_wconf 89'>a</span>
<span class='ocrx_word' id='word_1_3' title='bbox 1254 153 1406 235; x_wconf 92'>la</span>
<span class='ocrx_word' id='word_1_4' title='bbox 1500 153 2173 235; x_wconf 95'>Waterman</span>
</span>
</p>
</div>
<div class='ocr_separator' id='block_1_2' title="bbox 135 202 2180 295"></div>
<div class='ocr_carea' id='block_1_3' title="bbox 145 414 2929 1221">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 145 414 1154 516">
<span class='ocr_line' id='line_1_2' title="bbox 145 414 1154 516; baseline -0.005 -4; x_size 122.38636; x_descenders 24.386362; x_ascenders 40">
<span class='ocrx_word' id='word_1_5' title='bbox 145 414 211 512; x_wconf 60'>4</span>
<span class='ocrx_word' id='word_1_6' title='bbox 318 453 551 516; x_wconf 93'>ons</span>
<span class='ocrx_word' id='word_1_7' title='bbox 660 430 1154 512; x_wconf 91'>linzen</span>
</span>
</p>
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 146 568 1239 667">
<span class='ocr_line' id='line_1_3' title="bbox 146 568 1239 667; baseline -0.004 -16; x_size 99; x_descenders 17; x_ascenders 24">
<span class='ocrx_word' id='word_1_8' title='bbox 146 569 209 667; x_wconf 94'>3</span>
<span class='ocrx_word' id='word_1_9' title='bbox 323 568 729 652; x_wconf 83'>liter</span>
<span class='ocrx_word' id='word_1_10' title='bbox 821 569 1239 650; x_wconf 96'>water</span>
</span>
</p>
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 148 705 645 804">
<span class='ocr_line' id='line_1_4' title="bbox 148 705 645 804; baseline -0.016 -14; x_size 103; x_descenders 19; x_ascenders 27">
<span class='ocrx_word' id='word_1_11' title='bbox 148 706 211 804; x_wconf 88'>3</span>
<span class='ocrx_word' id='word_1_12' title='bbox 311 705 645 789; x_wconf 52'>uien</span>
</span>
</p>
<p class='ocr_par' id='par_1_5' lang='eng' title="bbox 147 832 1154 950">
<span class='ocr_line' id='line_1_5' title="bbox 147 832 1154 950; baseline -0.004 -27; x_size 118; x_descenders 28; x_ascenders 30">
<span class='ocrx_word' id='word_1_13' title='bbox 147 832 623 950; x_wconf 91'>bloem,</span>
<span class='ocrx_word' id='word_1_14' title='bbox 737 843 1154 924; x_wconf 91'>boter</span>
</span>
</p>
<p class='ocr_par' id='par_1_6' lang='eng' title="bbox 148 979 1241 1087">
<span class='ocr_line' id='line_1_6' title="bbox 148 979 1241 1087; baseline -0.005 -21; x_size 107; x_descenders 24; x_ascenders 24">
<span class='ocrx_word' id='word_1_15' title='bbox 148 983 215 1066; x_wconf 88'>2</span>
<span class='ocrx_word' id='word_1_16' title='bbox 312 983 807 1087; x_wconf 88'>kopjes</span>
<span class='ocrx_word' id='word_1_17' title='bbox 905 979 1241 1062; x_wconf 92'>melk</span>
</span>
</p>
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 154 1109 2929 1221">
<span class='ocr_line' id='line_1_7' title="bbox 154 1109 2929 1221; baseline -0.003 -21; x_size 112; x_descenders 28; x_ascenders 26">
<span class='ocrx_word' id='word_1_18' title='bbox 154 1117 791 1221; x_wconf 92'>laurier,</span>
<span class='ocrx_word' id='word_1_19' title='bbox 906 1111 1810 1219; x_wconf 90'>kruidnagel,</span>
<span class='ocrx_word' id='word_1_20' title='bbox 1927 1109 2490 1219; x_wconf 90'>kerrie,</span>
<span class='ocrx_word' id='word_1_21' title='bbox 2615 1110 2929 1195; x_wconf 91'>zout</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_4' title="bbox 147 1383 3706 2731">
<p class='ocr_par' id='par_1_8' lang='eng' title="bbox 147 1383 3706 2731">
<span class='ocr_line' id='line_1_8' title="bbox 149 1383 3612 1474; baseline -0.003 -2; x_size 107.01524; x_descenders 25.01524; x_ascenders 21">
<span class='ocrx_word' id='word_1_22' title='bbox 149 1395 303 1474; x_wconf 93'>De</span>
<span class='ocrx_word' id='word_1_23' title='bbox 411 1390 902 1473; x_wconf 80'>linzgen</span>
<span class='ocrx_word' id='word_1_24' title='bbox 996 1409 1497 1470; x_wconf 89'>wassen</span>
<span class='ocrx_word' id='word_1_25' title='bbox 1595 1409 1752 1470; x_wconf 88'>en</span>
<span class='ocrx_word' id='word_1_26' title='bbox 1853 1385 2166 1467; x_wconf 75'>in-l</span>
<span class='ocrx_word' id='word_1_27' title='bbox 2275 1383 2684 1466; x_wconf 93'>liter</span>
<span class='ocrx_word' id='word_1_28' title='bbox 2775 1383 3278 1464; x_wconf 91'>kokend</span>
<span class='ocrx_word' id='word_1_29' title='bbox 3368 1401 3612 1462; x_wconf 90'>wa-</span>
</span>
<span class='ocr_line' id='line_1_9' title="bbox 157 1516 3520 1632; baseline -0.004 -21; x_size 110; x_descenders 25; x_ascenders 25">
<span class='ocrx_word' id='word_1_30' title='bbox 157 1531 394 1611; x_wconf 94'>ter</span>
<span class='ocrx_word' id='word_1_31' title='bbox 495 1527 558 1609; x_wconf 93'>1</span>
<span class='ocrx_word' id='word_1_32' title='bbox 658 1530 903 1632; x_wconf 84'>dag</span>
<span class='ocrx_word' id='word_1_33' title='bbox 1002 1527 1415 1609; x_wconf 90'>laten</span>
<span class='ocrx_word' id='word_1_34' title='bbox 1505 1525 1979 1611; x_wconf 58'>weken,</span>
<span class='ocrx_word' id='word_1_35' title='bbox 2103 1521 2167 1603; x_wconf 96'>2</span>
<span class='ocrx_word' id='word_1_36' title='bbox 2275 1518 2683 1603; x_wconf 83'>liter</span>
<span class='ocrx_word' id='word_1_37' title='bbox 2777 1519 3194 1601; x_wconf 96'>water</span>
<span class='ocrx_word' id='word_1_38' title='bbox 3286 1516 3520 1599; x_wconf 89'>bij</span>
</span>
<span class='ocr_line' id='line_1_10' title="bbox 152 1651 3616 1767; baseline -0.004 -21; x_size 110; x_descenders 27; x_ascenders 23">
<span class='ocrx_word' id='word_1_39' title='bbox 152 1668 302 1747; x_wconf 92'>de</span>
<span class='ocrx_word' id='word_1_40' title='bbox 407 1662 905 1747; x_wconf 91'>linzen</span>
<span class='ocrx_word' id='word_1_41' title='bbox 996 1682 1559 1767; x_wconf 92'>voegen,</span>
<span class='ocrx_word' id='word_1_42' title='bbox 1680 1659 2176 1741; x_wconf 96'>zonder</span>
<span class='ocrx_word' id='word_1_43' title='bbox 2267 1655 2506 1737; x_wconf 96'>het</span>
<span class='ocrx_word' id='word_1_44' title='bbox 2606 1655 3023 1737; x_wconf 92'>water</span>
<span class='ocrx_word' id='word_1_45' title='bbox 3116 1651 3616 1735; x_wconf 91'>waarin</span>
</span>
<span class='ocr_line' id='line_1_11' title="bbox 153 1782 3704 1905; baseline -0.004 -20; x_size 117; x_descenders 23; x_ascenders 34">
<span class='ocrx_word' id='word_1_46' title='bbox 153 1824 305 1885; x_wconf 91'>ze</span>
<span class='ocrx_word' id='word_1_47' title='bbox 407 1800 979 1905; x_wconf 85'>geweekt</span>
<span class='ocrx_word' id='word_1_48' title='bbox 1089 1797 1412 1900; x_wconf 96'>zijn</span>
<span class='ocrx_word' id='word_1_49' title='bbox 1510 1796 1672 1878; x_wconf 96'>af</span>
<span class='ocrx_word' id='word_1_50' title='bbox 1770 1782 1914 1876; x_wconf 93'>te</span>
<span class='ocrx_word' id='word_1_51' title='bbox 2019 1792 2576 1899; x_wconf 54'>gieten,</span>
<span class='ocrx_word' id='word_1_52' title='bbox 2690 1790 2850 1874; x_wconf 93'>De</span>
<span class='ocrx_word' id='word_1_53' title='bbox 2948 1791 3357 1872; x_wconf 89'>helft</span>
<span class='ocrx_word' id='word_1_54' title='bbox 3452 1811 3704 1873; x_wconf 96'>van</span>
</span>
<span class='ocr_line' id='line_1_12' title="bbox 151 1928 3593 2035; baseline -0.003 -13; x_size 109; x_descenders 25; x_ascenders 25">
<span class='ocrx_word' id='word_1_55' title='bbox 151 1942 305 2024; x_wconf 92'>de</span>
<span class='ocrx_word' id='word_1_56' title='bbox 403 1940 735 2021; x_wconf 89'>uien</span>
<span class='ocrx_word' id='word_1_57' title='bbox 829 1938 1330 2022; x_wconf 91'>bakken</span>
<span class='ocrx_word' id='word_1_58' title='bbox 1419 1939 1659 2018; x_wconf 92'>met</span>
<span class='ocrx_word' id='word_1_59' title='bbox 1770 1933 2346 2016; x_wconf 91'>laurier</span>
<span class='ocrx_word' id='word_1_60' title='bbox 2447 1953 2603 2014; x_wconf 91'>en</span>
<span class='ocrx_word' id='word_1_61' title='bbox 2691 1928 3593 2035; x_wconf 63'>Kruidnagel.</span>
</span>
<span class='ocr_line' id='line_1_13' title="bbox 151 2067 3451 2180; baseline -0.004 -21; x_size 108; x_descenders 25; x_ascenders 24">
<span class='ocrx_word' id='word_1_62' title='bbox 151 2076 473 2159; x_wconf 92'>Alle</span>
<span class='ocrx_word' id='word_1_63' title='bbox 569 2076 965 2180; x_wconf 88'>uien,</span>
<span class='ocrx_word' id='word_1_64' title='bbox 1079 2072 1578 2156; x_wconf 90'>kerrie</span>
<span class='ocrx_word' id='word_1_65' title='bbox 1685 2092 1837 2153; x_wconf 93'>en</span>
<span class='ocrx_word' id='word_1_66' title='bbox 1938 2072 2254 2153; x_wconf 81'>zgout</span>
<span class='ocrx_word' id='word_1_67' title='bbox 2355 2068 2593 2172; x_wconf 43'>bij</span>
<span class='ocrx_word' id='word_1_68' title='bbox 2696 2071 2850 2150; x_wconf 91'>de</span>
<span class='ocrx_word' id='word_1_69' title='bbox 2957 2067 3451 2150; x_wconf 85'>linzen</span>
</span>
<span class='ocr_line' id='line_1_14' title="bbox 147 2205 3614 2318; baseline -0.003 -23; x_size 106; x_descenders 22; x_ascenders 25">
<span class='ocrx_word' id='word_1_70' title='bbox 147 2234 711 2318; x_wconf 88'>voegen,</span>
<span class='ocrx_word' id='word_1_71' title='bbox 826 2210 1234 2295; x_wconf 95'>Alles</span>
<span class='ocrx_word' id='word_1_72' title='bbox 1342 2232 1588 2295; x_wconf 95'>aan</span>
<span class='ocrx_word' id='word_1_73' title='bbox 1679 2212 1831 2291; x_wconf 96'>de</span>
<span class='ocrx_word' id='word_1_74' title='bbox 1928 2208 2263 2290; x_wconf 93'>kook</span>
<span class='ocrx_word' id='word_1_75' title='bbox 2355 2206 3000 2308; x_wconf 54'>brengen,</span>
<span class='ocrx_word' id='word_1_76' title='bbox 3117 2205 3367 2285; x_wconf 95'>Van</span>
<span class='ocrx_word' id='word_1_77' title='bbox 3462 2206 3614 2287; x_wconf 95'>de</span>
</span>
<span class='ocr_line' id='line_1_15' title="bbox 152 2341 3706 2447; baseline -0.003 -18; x_size 107; x_descenders 24; x_ascenders 24">
<span class='ocrx_word' id='word_1_78' title='bbox 152 2352 565 2434; x_wconf 91'>bloem</span>
<span class='ocrx_word' id='word_1_79' title='bbox 655 2351 896 2431; x_wconf 92'>met</span>
<span class='ocrx_word' id='word_1_80' title='bbox 997 2349 1669 2431; x_wconf 90'>boter</span>
<span class='ocrx_word' id='word_1_81' title='bbox 1514 2337 1683 2455; x_wconf 91'>en</span>
<span class='ocrx_word' id='word_1_82' title='bbox 1757 2343 2095 2426; x_wconf 88'>melk</span>
<span class='ocrx_word' id='word_1_83' title='bbox 2193 2364 2432 2427; x_wconf 93'>een</span>
<span class='ocrx_word' id='word_1_84' title='bbox 2527 2341 2935 2447; x_wconf 90'>papje</span>
<span class='ocrx_word' id='word_1_85' title='bbox 3029 2341 3453 2422; x_wconf 96'>maken</span>
<span class='ocrx_word' id='word_1_86' title='bbox 3549 2362 3706 2422; x_wconf 95'>en</span>
</span>
<span class='ocr_line' id='line_1_16' title="bbox 149 2477 3619 2586; baseline -0.003 -16; x_size 107; x_descenders 24; x_ascenders 23">
<span class='ocrx_word' id='word_1_87' title='bbox 149 2489 650 2571; x_wconf 86'>verder</span>
<span class='ocrx_word' id='word_1_88' title='bbox 750 2486 1330 2570; x_wconf 90'>afmaken</span>
<span class='ocrx_word' id='word_1_89' title='bbox 1420 2485 1660 2567; x_wconf 96'>met</span>
<span class='ocrx_word' id='word_1_90' title='bbox 1765 2485 1917 2566; x_wconf 93'>de</span>
<span class='ocrx_word' id='word_1_91' title='bbox 2025 2505 2409 2586; x_wconf 86'>soep,</span>
<span class='ocrx_word' id='word_1_92' title='bbox 2521 2479 2764 2564; x_wconf 96'>Als</span>
<span class='ocrx_word' id='word_1_93' title='bbox 2868 2480 3021 2561; x_wconf 92'>de</span>
<span class='ocrx_word' id='word_1_94' title='bbox 3127 2477 3619 2562; x_wconf 91'>linzen</span>
</span>
<span class='ocr_line' id='line_1_17' title="bbox 155 2619 2412 2731; baseline -0.005 -20; x_size 98; x_descenders 15; x_ascenders 23">
<span class='ocrx_word' id='word_1_95' title='bbox 155 2647 481 2731; x_wconf 35'>gfgaar</span>
<span class='ocrx_word' id='word_1_96' title='bbox 581 2624 909 2728; x_wconf 87'>Zijn</span>
<span class='ocrx_word' id='word_1_97' title='bbox 1005 2623 1153 2707; x_wconf 95'>is</span>
<span class='ocrx_word' id='word_1_98' title='bbox 1255 2624 1409 2706; x_wconf 93'>de</span>
<span class='ocrx_word' id='word_1_99' title='bbox 1522 2642 1840 2726; x_wconf 91'>soep</span>
<span class='ocrx_word' id='word_1_100' title='bbox 1929 2619 2412 2709; x_wconf 89'>klaar.</span>
</span>
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,24 @@
i a la Waterman
4 ons linzen
3 liter water
3 uien
bloem, boter
2 kopjes melk
laurier, kruidnagel, kerrie, zout
De linzgen wassen en in-l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruidnagel.
Alle uien, kerrie en zgout bij de linzen
voegen, Alles aan de kook brengen, Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gfgaar Zijn is de soep klaar.
@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.3.2' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.vh2to5iv/000001_ocr.png"; bbox 0 0 640 682; ppageno 0; scan_res 230 230'>
<div class='ocr_carea' id='block_1_1' title="bbox 365 15 429 29">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 365 15 429 29">
<span class='ocr_line' id='line_1_1' title="bbox 365 15 429 29; baseline 0 0; x_size 19.238094; x_descenders 5.2380953; x_ascenders 4">
<span class='ocrx_word' id='word_1_1' title='bbox 365 15 429 29; x_wconf 92'>Tarnose</span>
</span>
</p>
</div>
<div class='ocr_photo' id='block_1_2' title="bbox 255 38 616 72"></div>
<div class='ocr_photo' id='block_1_3' title="bbox 273 38 349 57"></div>
<div class='ocr_carea' id='block_1_4' title="bbox 186 20 244 49">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 186 20 345 49">
<span class='ocr_line' id='line_1_2' title="bbox 186 20 244 49; baseline -0.006 -10; x_size 20.454546; x_descenders 5.4545455; x_ascenders 5">
<span class='ocrx_word' id='word_1_2' title='bbox 186 20 244 49; x_wconf 92'>Bokale</span>
<span class='ocrx_word' id='word_1_3' title='bbox 299 28 345 46; x_wconf 42'>oa</span>
</span>
</p>
</div>
<div class='ocr_photo' id='block_1_5' title="bbox 64 38 186 72"></div>
<div class='ocr_carea' id='block_1_6' title="bbox 537 276 612 291">
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 537 276 612 291">
<span class='ocr_line' id='line_1_3' title="bbox 537 276 612 291; baseline 0 0; x_size 20.238094; x_descenders 5.2380953; x_ascenders 5">
<span class='ocrx_word' id='word_1_4' title='bbox 537 276 612 291; x_wconf 92'>Lehuntze</span>
</span>
</p>
</div>
<div class='ocr_photo' id='block_1_7' title="bbox 20 269 616 314"></div>
<div class='ocr_carea' id='block_1_8' title="bbox 480 323 550 341">
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 480 323 550 341">
<span class='ocr_line' id='line_1_4' title="bbox 480 323 550 341; baseline 0 -4; x_size 18; x_descenders 4; x_ascenders 4">
<span class='ocrx_word' id='word_1_5' title='bbox 480 323 550 341; x_wconf 91'>Mugerre</span>
</span>
</p>
</div>
<div class='ocr_photo' id='block_1_9' title="bbox 44 574 197 623"></div>
<div class='ocr_carea' id='block_1_10' title="bbox 204 542 603 574">
<p class='ocr_par' id='par_1_5' lang='eng' title="bbox 204 542 603 574">
<span class='ocr_line' id='line_1_5' title="bbox 204 542 603 574; baseline 0.018 -11; x_size 23.26087; x_descenders 5.2608695; x_ascenders 7">
<span class='ocrx_word' id='word_1_6' title='bbox 204 542 295 574; x_wconf 91'>Milafranga</span>
<span class='ocrx_word' id='word_1_7' title='bbox 439 552 603 569; x_wconf 90'>Komunikabideak</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_11' title="bbox 220 584 616 619">
<p class='ocr_par' id='par_1_6' lang='eng' title="bbox 220 584 616 625">
<span class='ocr_line' id='line_1_6' title="bbox 220 584 616 619; baseline -0.005 -1; x_size 43.666668; x_descenders 10.916667; x_ascenders 10.916667">
<span class='ocrx_word' id='word_1_8' title='bbox 220 585 404 619; x_wconf 2'>BAIONA</span>
<span class='ocrx_word' id='word_1_9' title='bbox 468 584 576 625; x_wconf 0'> zeiteninsiie</span>
<span class='ocrx_word' id='word_1_10' title='bbox 585 588 616 610; x_wconf 86'>—</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_12' title="bbox 393 623 598 634">
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 393 623 598 634">
<span class='ocr_line' id='line_1_7' title="bbox 393 623 598 634; baseline 0 0; x_size 22.75; x_descenders 5.5; x_ascenders 5.75">
<span class='ocrx_word' id='word_1_11' title='bbox 393 629 396 631; x_wconf 54'>7</span>
<span class='ocrx_word' id='word_1_12' title='bbox 470 623 539 634; x_wconf 24'>Trenbideak</span>
<span class='ocrx_word' id='word_1_13' title='bbox 550 628 598 630; x_wconf 24'>-----</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_13' title="bbox 81 633 613 667">
<p class='ocr_par' id='par_1_8' lang='eng' title="bbox 81 633 613 667">
<span class='ocr_line' id='line_1_8' title="bbox 81 633 613 667; baseline -0.002 -13; x_size 21; x_descenders 4; x_ascenders 7">
<span class='ocrx_word' id='word_1_14' title='bbox 81 637 111 653; x_wconf 0'>t\</span>
<span class='ocrx_word' id='word_1_15' title='bbox 123 633 201 663; x_wconf 22'>Basusarri</span>
<span class='ocrx_word' id='word_1_16' title='bbox 214 633 222 663; x_wconf 0'>—</span>
<span class='ocrx_word' id='word_1_17' title='bbox 230 647 325 656; x_wconf 0'>spmeans:20141004</span>
<span class='ocrx_word' id='word_1_18' title='bbox 373 633 415 667; x_wconf 42'>ae:</span>
<span class='ocrx_word' id='word_1_19' title='bbox 441 650 444 653; x_wconf 25'>.</span>
<span class='ocrx_word' id='word_1_20' title='bbox 521 649 544 657; x_wconf 17'>_</span>
<span class='ocrx_word' id='word_1_21' title='bbox 595 650 613 659; x_wconf 7'>~</span>
</span>
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,15 @@
Tarnose
Bokale oa
Lehuntze
Mugerre
Milafranga Komunikabideak
BAIONA zeiteninsiie —
7 Trenbideak -----
t\ Basusarri — spmeans:20141004 ae: . _ ~
@@ -0,0 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.3.2' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.vh2to5iv/000002_ocr.png"; bbox 0 0 400 50; ppageno 0; scan_res 200 200'>
<div class='ocr_carea' id='block_1_1' title="bbox 54 16 344 33">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 54 16 344 33">
<span class='ocr_line' id='line_1_1' title="bbox 54 16 344 33; baseline 0 -4; x_size 25.75; x_descenders 5.5; x_ascenders 6.75">
<span class='ocrx_word' id='word_1_1' title='bbox 54 16 110 29; x_wconf 84'>Covfefe</span>
<span class='ocrx_word' id='word_1_2' title='bbox 115 17 127 29; x_wconf 95'>is</span>
<span class='ocrx_word' id='word_1_3' title='bbox 132 20 140 29; x_wconf 95'>a</span>
<span class='ocrx_word' id='word_1_4' title='bbox 145 16 212 33; x_wconf 85'>perfectly</span>
<span class='ocrx_word' id='word_1_5' title='bbox 217 16 296 29; x_wconf 85'>cromulent</span>
<span class='ocrx_word' id='word_1_6' title='bbox 300 16 344 29; x_wconf 95'>word.</span>
</span>
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1 @@
Covfefe is a perfectly cromulent word.
@@ -0,0 +1,177 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.3.2' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.vh2to5iv/000003_ocr.png"; bbox 0 0 4000 2864; ppageno 0; scan_res 1440 1440'>
<div class='ocr_carea' id='block_1_1' title="bbox 142 146 2173 258">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 142 146 2173 258">
<span class='ocr_line' id='line_1_1' title="bbox 142 146 2173 258; baseline -0.004 -19; x_size 113; x_descenders 23; x_ascenders 32">
<span class='ocrx_word' id='word_1_1' title='bbox 142 154 982 258; x_wconf 90'>Linzensoep</span>
<span class='ocrx_word' id='word_1_2' title='bbox 1080 146 1152 237; x_wconf 96'>a</span>
<span class='ocrx_word' id='word_1_3' title='bbox 1254 153 1406 235; x_wconf 96'>la</span>
<span class='ocrx_word' id='word_1_4' title='bbox 1500 153 2173 235; x_wconf 96'>Waterman</span>
</span>
</p>
</div>
<div class='ocr_separator' id='block_1_2' title="bbox 136 253 2180 275"></div>
<div class='ocr_carea' id='block_1_3' title="bbox 145 414 2929 1221">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 145 414 1154 516">
<span class='ocr_line' id='line_1_2' title="bbox 145 414 1154 516; baseline -0.005 -4; x_size 122.38636; x_descenders 24.386362; x_ascenders 40">
<span class='ocrx_word' id='word_1_5' title='bbox 145 414 211 512; x_wconf 60'>4</span>
<span class='ocrx_word' id='word_1_6' title='bbox 318 453 551 516; x_wconf 93'>ons</span>
<span class='ocrx_word' id='word_1_7' title='bbox 660 430 1154 512; x_wconf 91'>linzen</span>
</span>
</p>
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 146 568 1239 667">
<span class='ocr_line' id='line_1_3' title="bbox 146 568 1239 667; baseline -0.004 -16; x_size 99; x_descenders 17; x_ascenders 24">
<span class='ocrx_word' id='word_1_8' title='bbox 146 569 209 667; x_wconf 94'>3</span>
<span class='ocrx_word' id='word_1_9' title='bbox 323 568 729 652; x_wconf 83'>liter</span>
<span class='ocrx_word' id='word_1_10' title='bbox 821 569 1239 650; x_wconf 96'>water</span>
</span>
</p>
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 148 705 645 804">
<span class='ocr_line' id='line_1_4' title="bbox 148 705 645 804; baseline -0.016 -14; x_size 103; x_descenders 19; x_ascenders 27">
<span class='ocrx_word' id='word_1_11' title='bbox 148 706 211 804; x_wconf 88'>3</span>
<span class='ocrx_word' id='word_1_12' title='bbox 311 705 645 789; x_wconf 52'>uien</span>
</span>
</p>
<p class='ocr_par' id='par_1_5' lang='eng' title="bbox 147 832 1154 950">
<span class='ocr_line' id='line_1_5' title="bbox 147 832 1154 950; baseline -0.004 -27; x_size 118; x_descenders 28; x_ascenders 30">
<span class='ocrx_word' id='word_1_13' title='bbox 147 832 623 950; x_wconf 91'>bloem,</span>
<span class='ocrx_word' id='word_1_14' title='bbox 737 843 1154 924; x_wconf 91'>boter</span>
</span>
</p>
<p class='ocr_par' id='par_1_6' lang='eng' title="bbox 148 979 1241 1087">
<span class='ocr_line' id='line_1_6' title="bbox 148 979 1241 1087; baseline -0.005 -21; x_size 107; x_descenders 24; x_ascenders 24">
<span class='ocrx_word' id='word_1_15' title='bbox 148 983 215 1066; x_wconf 88'>2</span>
<span class='ocrx_word' id='word_1_16' title='bbox 312 983 807 1087; x_wconf 88'>kopjes</span>
<span class='ocrx_word' id='word_1_17' title='bbox 905 979 1241 1062; x_wconf 92'>melk</span>
</span>
</p>
<p class='ocr_par' id='par_1_7' lang='eng' title="bbox 154 1109 2929 1221">
<span class='ocr_line' id='line_1_7' title="bbox 154 1109 2929 1221; baseline -0.003 -21; x_size 112; x_descenders 28; x_ascenders 26">
<span class='ocrx_word' id='word_1_18' title='bbox 154 1117 791 1221; x_wconf 92'>laurier,</span>
<span class='ocrx_word' id='word_1_19' title='bbox 906 1111 1810 1219; x_wconf 90'>kruidnagel,</span>
<span class='ocrx_word' id='word_1_20' title='bbox 1927 1109 2490 1219; x_wconf 90'>kerrie,</span>
<span class='ocrx_word' id='word_1_21' title='bbox 2615 1110 2929 1195; x_wconf 91'>zout</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_4' title="bbox 147 1383 3706 2731">
<p class='ocr_par' id='par_1_8' lang='eng' title="bbox 147 1383 3706 2731">
<span class='ocr_line' id='line_1_8' title="bbox 149 1383 3612 1474; baseline -0.003 -2; x_size 107.01524; x_descenders 25.01524; x_ascenders 21">
<span class='ocrx_word' id='word_1_22' title='bbox 149 1395 303 1474; x_wconf 93'>De</span>
<span class='ocrx_word' id='word_1_23' title='bbox 411 1390 902 1473; x_wconf 80'>linzgen</span>
<span class='ocrx_word' id='word_1_24' title='bbox 996 1409 1497 1470; x_wconf 89'>wassen</span>
<span class='ocrx_word' id='word_1_25' title='bbox 1595 1409 1752 1470; x_wconf 88'>en</span>
<span class='ocrx_word' id='word_1_26' title='bbox 1853 1385 2166 1467; x_wconf 75'>in-l</span>
<span class='ocrx_word' id='word_1_27' title='bbox 2275 1383 2684 1466; x_wconf 93'>liter</span>
<span class='ocrx_word' id='word_1_28' title='bbox 2775 1383 3278 1464; x_wconf 91'>kokend</span>
<span class='ocrx_word' id='word_1_29' title='bbox 3368 1401 3612 1462; x_wconf 90'>wa-</span>
</span>
<span class='ocr_line' id='line_1_9' title="bbox 157 1516 3521 1632; baseline -0.004 -21; x_size 110; x_descenders 25; x_ascenders 25">
<span class='ocrx_word' id='word_1_30' title='bbox 157 1531 394 1611; x_wconf 93'>ter</span>
<span class='ocrx_word' id='word_1_31' title='bbox 495 1527 558 1609; x_wconf 94'>1</span>
<span class='ocrx_word' id='word_1_32' title='bbox 658 1530 903 1632; x_wconf 75'>dag</span>
<span class='ocrx_word' id='word_1_33' title='bbox 1002 1527 1415 1609; x_wconf 85'>laten</span>
<span class='ocrx_word' id='word_1_34' title='bbox 1505 1525 1979 1611; x_wconf 86'>weken,</span>
<span class='ocrx_word' id='word_1_35' title='bbox 2103 1521 2167 1603; x_wconf 96'>2</span>
<span class='ocrx_word' id='word_1_36' title='bbox 2275 1518 2683 1603; x_wconf 93'>liter</span>
<span class='ocrx_word' id='word_1_37' title='bbox 2777 1519 3194 1601; x_wconf 96'>water</span>
<span class='ocrx_word' id='word_1_38' title='bbox 3286 1516 3521 1620; x_wconf 93'>bij</span>
</span>
<span class='ocr_line' id='line_1_10' title="bbox 152 1651 3616 1767; baseline -0.004 -21; x_size 110; x_descenders 27; x_ascenders 23">
<span class='ocrx_word' id='word_1_39' title='bbox 152 1668 302 1747; x_wconf 92'>de</span>
<span class='ocrx_word' id='word_1_40' title='bbox 407 1662 905 1747; x_wconf 91'>linzen</span>
<span class='ocrx_word' id='word_1_41' title='bbox 996 1682 1559 1767; x_wconf 92'>voegen,</span>
<span class='ocrx_word' id='word_1_42' title='bbox 1680 1659 2176 1741; x_wconf 96'>zonder</span>
<span class='ocrx_word' id='word_1_43' title='bbox 2267 1655 2506 1737; x_wconf 96'>het</span>
<span class='ocrx_word' id='word_1_44' title='bbox 2606 1655 3023 1737; x_wconf 92'>water</span>
<span class='ocrx_word' id='word_1_45' title='bbox 3116 1651 3616 1735; x_wconf 91'>waarin</span>
</span>
<span class='ocr_line' id='line_1_11' title="bbox 153 1782 3704 1905; baseline -0.004 -20; x_size 117; x_descenders 23; x_ascenders 34">
<span class='ocrx_word' id='word_1_46' title='bbox 153 1824 305 1885; x_wconf 91'>ze</span>
<span class='ocrx_word' id='word_1_47' title='bbox 407 1800 979 1905; x_wconf 85'>geweekt</span>
<span class='ocrx_word' id='word_1_48' title='bbox 1089 1797 1412 1900; x_wconf 96'>zijn</span>
<span class='ocrx_word' id='word_1_49' title='bbox 1510 1796 1672 1878; x_wconf 96'>af</span>
<span class='ocrx_word' id='word_1_50' title='bbox 1770 1782 1914 1876; x_wconf 93'>te</span>
<span class='ocrx_word' id='word_1_51' title='bbox 2019 1792 2576 1899; x_wconf 54'>gieten,</span>
<span class='ocrx_word' id='word_1_52' title='bbox 2690 1790 2850 1874; x_wconf 93'>De</span>
<span class='ocrx_word' id='word_1_53' title='bbox 2948 1791 3357 1872; x_wconf 89'>helft</span>
<span class='ocrx_word' id='word_1_54' title='bbox 3452 1811 3704 1873; x_wconf 96'>van</span>
</span>
<span class='ocr_line' id='line_1_12' title="bbox 151 1928 3593 2035; baseline -0.003 -13; x_size 109; x_descenders 25; x_ascenders 25">
<span class='ocrx_word' id='word_1_55' title='bbox 151 1942 305 2024; x_wconf 92'>de</span>
<span class='ocrx_word' id='word_1_56' title='bbox 403 1940 735 2021; x_wconf 89'>uien</span>
<span class='ocrx_word' id='word_1_57' title='bbox 829 1938 1330 2022; x_wconf 91'>bakken</span>
<span class='ocrx_word' id='word_1_58' title='bbox 1419 1939 1659 2018; x_wconf 92'>met</span>
<span class='ocrx_word' id='word_1_59' title='bbox 1770 1933 2346 2016; x_wconf 91'>laurier</span>
<span class='ocrx_word' id='word_1_60' title='bbox 2447 1953 2603 2014; x_wconf 91'>en</span>
<span class='ocrx_word' id='word_1_61' title='bbox 2691 1928 3593 2035; x_wconf 63'>Kruidnagel.</span>
</span>
<span class='ocr_line' id='line_1_13' title="bbox 151 2067 3451 2180; baseline -0.004 -21; x_size 108; x_descenders 25; x_ascenders 24">
<span class='ocrx_word' id='word_1_62' title='bbox 151 2076 473 2159; x_wconf 92'>Alle</span>
<span class='ocrx_word' id='word_1_63' title='bbox 569 2076 965 2180; x_wconf 88'>uien,</span>
<span class='ocrx_word' id='word_1_64' title='bbox 1079 2072 1578 2156; x_wconf 90'>kerrie</span>
<span class='ocrx_word' id='word_1_65' title='bbox 1685 2092 1837 2153; x_wconf 93'>en</span>
<span class='ocrx_word' id='word_1_66' title='bbox 1938 2072 2254 2153; x_wconf 81'>zgout</span>
<span class='ocrx_word' id='word_1_67' title='bbox 2355 2068 2593 2172; x_wconf 43'>bij</span>
<span class='ocrx_word' id='word_1_68' title='bbox 2696 2071 2850 2150; x_wconf 91'>de</span>
<span class='ocrx_word' id='word_1_69' title='bbox 2957 2067 3451 2150; x_wconf 85'>linzen</span>
</span>
<span class='ocr_line' id='line_1_14' title="bbox 147 2205 3614 2318; baseline -0.003 -23; x_size 106; x_descenders 22; x_ascenders 25">
<span class='ocrx_word' id='word_1_70' title='bbox 147 2234 711 2318; x_wconf 88'>voegen,</span>
<span class='ocrx_word' id='word_1_71' title='bbox 826 2210 1234 2295; x_wconf 95'>Alles</span>
<span class='ocrx_word' id='word_1_72' title='bbox 1342 2232 1588 2295; x_wconf 95'>aan</span>
<span class='ocrx_word' id='word_1_73' title='bbox 1679 2212 1831 2291; x_wconf 96'>de</span>
<span class='ocrx_word' id='word_1_74' title='bbox 1928 2208 2263 2290; x_wconf 93'>kook</span>
<span class='ocrx_word' id='word_1_75' title='bbox 2355 2206 3000 2308; x_wconf 54'>brengen,</span>
<span class='ocrx_word' id='word_1_76' title='bbox 3117 2205 3367 2285; x_wconf 95'>Van</span>
<span class='ocrx_word' id='word_1_77' title='bbox 3462 2206 3614 2287; x_wconf 95'>de</span>
</span>
<span class='ocr_line' id='line_1_15' title="bbox 152 2341 3706 2447; baseline -0.003 -18; x_size 107; x_descenders 24; x_ascenders 24">
<span class='ocrx_word' id='word_1_78' title='bbox 152 2352 565 2434; x_wconf 91'>bloem</span>
<span class='ocrx_word' id='word_1_79' title='bbox 655 2351 896 2431; x_wconf 92'>met</span>
<span class='ocrx_word' id='word_1_80' title='bbox 997 2349 1669 2431; x_wconf 90'>boter</span>
<span class='ocrx_word' id='word_1_81' title='bbox 1514 2337 1683 2455; x_wconf 91'>en</span>
<span class='ocrx_word' id='word_1_82' title='bbox 1757 2343 2095 2426; x_wconf 88'>melk</span>
<span class='ocrx_word' id='word_1_83' title='bbox 2193 2364 2432 2427; x_wconf 93'>een</span>
<span class='ocrx_word' id='word_1_84' title='bbox 2527 2341 2935 2447; x_wconf 90'>papje</span>
<span class='ocrx_word' id='word_1_85' title='bbox 3029 2341 3453 2422; x_wconf 96'>maken</span>
<span class='ocrx_word' id='word_1_86' title='bbox 3549 2362 3706 2422; x_wconf 95'>en</span>
</span>
<span class='ocr_line' id='line_1_16' title="bbox 149 2477 3619 2586; baseline -0.003 -16; x_size 107; x_descenders 24; x_ascenders 23">
<span class='ocrx_word' id='word_1_87' title='bbox 149 2489 650 2571; x_wconf 86'>verder</span>
<span class='ocrx_word' id='word_1_88' title='bbox 750 2486 1330 2570; x_wconf 90'>afmaken</span>
<span class='ocrx_word' id='word_1_89' title='bbox 1420 2485 1660 2567; x_wconf 96'>met</span>
<span class='ocrx_word' id='word_1_90' title='bbox 1765 2485 1917 2566; x_wconf 93'>de</span>
<span class='ocrx_word' id='word_1_91' title='bbox 2025 2505 2409 2586; x_wconf 86'>soep,</span>
<span class='ocrx_word' id='word_1_92' title='bbox 2521 2479 2764 2564; x_wconf 96'>Als</span>
<span class='ocrx_word' id='word_1_93' title='bbox 2868 2480 3021 2561; x_wconf 92'>de</span>
<span class='ocrx_word' id='word_1_94' title='bbox 3127 2477 3619 2562; x_wconf 91'>linzen</span>
</span>
<span class='ocr_line' id='line_1_17' title="bbox 155 2619 2412 2731; baseline -0.005 -20; x_size 98; x_descenders 15; x_ascenders 23">
<span class='ocrx_word' id='word_1_95' title='bbox 155 2647 481 2731; x_wconf 35'>gfgaar</span>
<span class='ocrx_word' id='word_1_96' title='bbox 581 2624 909 2728; x_wconf 87'>Zijn</span>
<span class='ocrx_word' id='word_1_97' title='bbox 1005 2623 1153 2707; x_wconf 95'>is</span>
<span class='ocrx_word' id='word_1_98' title='bbox 1255 2624 1409 2706; x_wconf 93'>de</span>
<span class='ocrx_word' id='word_1_99' title='bbox 1522 2642 1840 2726; x_wconf 91'>soep</span>
<span class='ocrx_word' id='word_1_100' title='bbox 1929 2619 2412 2709; x_wconf 89'>klaar.</span>
</span>
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,24 @@
Linzensoep a la Waterman
4 ons linzen
3 liter water
3 uien
bloem, boter
2 kopjes melk
laurier, kruidnagel, kerrie, zout
De linzgen wassen en in-l liter kokend wa-
ter 1 dag laten weken, 2 liter water bij
de linzen voegen, zonder het water waarin
ze geweekt zijn af te gieten, De helft van
de uien bakken met laurier en Kruidnagel.
Alle uien, kerrie en zgout bij de linzen
voegen, Alles aan de kook brengen, Van de
bloem met boter en melk een papje maken en
verder afmaken met de soep, Als de linzen
gfgaar Zijn is de soep klaar.
@@ -9,7 +9,7 @@
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.9_hn68nz/000001_ocr.png"; bbox 0 0 1000 800; ppageno 0; scan_res 200 200'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.su6l_waz/000001_ocr.png"; bbox 0 0 1000 800; ppageno 0; scan_res 200 200'>
<div class='ocr_photo' id='block_1_1' title="bbox 296 96 704 504"></div>
<div class='ocr_carea' id='block_1_2' title="bbox 150 592 841 622">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 150 592 841 622">
@@ -9,7 +9,7 @@
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.g296cpyi/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -9,7 +9,7 @@
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000002_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.91dzclia/000002_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -9,7 +9,7 @@
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000003_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.91dzclia/000003_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -9,7 +9,7 @@
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.q0nk4qy2/000004_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.91dzclia/000004_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -9,7 +9,7 @@
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.zopxh6db/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.bgz1p10r/000001_ocr.png"; bbox 0 0 2550 3300; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 582 131 1968 303">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 582 131 1968 303">
<span class='ocr_header' id='line_1_1' title="bbox 882 131 1657 217; baseline 0.001 -17; x_size 85; x_descenders 16; x_ascenders 19">
@@ -0,0 +1,104 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.3.2' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.9pz7mnez/000001_ocr.png"; bbox 0 0 900 900; ppageno 0; scan_res 300 300'>
<div class='ocr_carea' id='block_1_1' title="bbox 180 159 760 740">
<p class='ocr_par' id='par_1_1' lang='deu' title="bbox 180 159 760 740">
<span class='ocr_line' id='line_1_1' title="bbox 182 159 721 198; baseline 0 -8; x_size 39; x_descenders 8; x_ascenders 10">
<span class='ocrx_word' id='word_1_1' title='bbox 182 160 292 190; x_wconf 92'>Portez</span>
<span class='ocrx_word' id='word_1_2' title='bbox 307 169 344 190; x_wconf 93'>ce</span>
<span class='ocrx_word' id='word_1_3' title='bbox 357 160 449 190; x_wconf 92'>vieux</span>
<span class='ocrx_word' id='word_1_4' title='bbox 462 159 583 197; x_wconf 91'>whisky</span>
<span class='ocrx_word' id='word_1_5' title='bbox 595 169 633 190; x_wconf 92'>au</span>
<span class='ocrx_word' id='word_1_6' title='bbox 646 160 721 198; x_wconf 87'>juge</span>
</span>
<span class='ocr_line' id='line_1_2' title="bbox 181 209 640 247; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
<span class='ocrx_word' id='word_1_7' title='bbox 181 209 276 240; x_wconf 95'>blond</span>
<span class='ocrx_word' id='word_1_8' title='bbox 293 210 343 247; x_wconf 93'>qui</span>
<span class='ocrx_word' id='word_1_9' title='bbox 358 210 448 240; x_wconf 91'>fume</span>
<span class='ocrx_word' id='word_1_10' title='bbox 463 219 516 240; x_wconf 96'>sur</span>
<span class='ocrx_word' id='word_1_11' title='bbox 529 219 587 240; x_wconf 96'>son</span>
<span class='ocrx_word' id='word_1_12' title='bbox 602 209 640 240; x_wconf 89'>Ile</span>
</span>
<span class='ocr_line' id='line_1_3' title="bbox 181 259 676 295; baseline 0 -5; x_size 36; x_descenders 5; x_ascenders 10">
<span class='ocrx_word' id='word_1_13' title='bbox 181 260 357 295; x_wconf 89'>interieure,</span>
<span class='ocrx_word' id='word_1_14' title='bbox 372 260 387 290; x_wconf 83'>a</span>
<span class='ocrx_word' id='word_1_15' title='bbox 404 260 478 290; x_wconf 61'>cöte</span>
<span class='ocrx_word' id='word_1_16' title='bbox 493 259 534 290; x_wconf 93'>de</span>
<span class='ocrx_word' id='word_1_17' title='bbox 551 259 676 290; x_wconf 81'>l&#39;alcöve</span>
</span>
<span class='ocr_line' id='line_1_4' title="bbox 180 309 613 345; baseline 0 -5; x_size 36; x_descenders 5; x_ascenders 10">
<span class='ocrx_word' id='word_1_18' title='bbox 180 309 305 345; x_wconf 92'>ovoide,</span>
<span class='ocrx_word' id='word_1_19' title='bbox 321 310 362 340; x_wconf 38'>oU</span>
<span class='ocrx_word' id='word_1_20' title='bbox 380 309 423 340; x_wconf 92'>les</span>
<span class='ocrx_word' id='word_1_21' title='bbox 440 309 562 340; x_wconf 92'>büches</span>
<span class='ocrx_word' id='word_1_22' title='bbox 577 319 613 340; x_wconf 96'>se</span>
</span>
<span class='ocr_line' id='line_1_5' title="bbox 180 359 693 397; baseline 0 -7; x_size 36; x_descenders 5; x_ascenders 10">
<span class='ocrx_word' id='word_1_23' title='bbox 180 362 375 390; x_wconf 92'>consument</span>
<span class='ocrx_word' id='word_1_24' title='bbox 389 359 469 390; x_wconf 92'>dans</span>
<span class='ocrx_word' id='word_1_25' title='bbox 485 359 575 395; x_wconf 86'>l&#39;ätre,</span>
<span class='ocrx_word' id='word_1_26' title='bbox 591 369 628 390; x_wconf 96'>ce</span>
<span class='ocrx_word' id='word_1_27' title='bbox 643 360 693 397; x_wconf 96'>qui</span>
</span>
<span class='ocr_line' id='line_1_6' title="bbox 181 409 617 447; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
<span class='ocrx_word' id='word_1_28' title='bbox 181 409 217 440; x_wconf 49'>ui</span>
<span class='ocrx_word' id='word_1_29' title='bbox 234 412 359 447; x_wconf 91'>permet</span>
<span class='ocrx_word' id='word_1_30' title='bbox 373 409 414 440; x_wconf 93'>de</span>
<span class='ocrx_word' id='word_1_31' title='bbox 430 419 548 447; x_wconf 90'>penser</span>
<span class='ocrx_word' id='word_1_32' title='bbox 556 405 568 451; x_wconf 73'>&amp;</span>
<span class='ocrx_word' id='word_1_33' title='bbox 594 409 617 440; x_wconf 96'>la</span>
</span>
<span class='ocr_line' id='line_1_7' title="bbox 180 459 680 498; baseline 0 -8; x_size 39; x_descenders 8; x_ascenders 10">
<span class='ocrx_word' id='word_1_34' title='bbox 180 460 402 498; x_wconf 79'>caenogenese</span>
<span class='ocrx_word' id='word_1_35' title='bbox 417 459 458 490; x_wconf 93'>de</span>
<span class='ocrx_word' id='word_1_36' title='bbox 475 459 557 490; x_wconf 42'>|&#39;etre</span>
<span class='ocrx_word' id='word_1_37' title='bbox 572 459 653 490; x_wconf 96'>dont</span>
<span class='ocrx_word' id='word_1_38' title='bbox 668 459 680 490; x_wconf 94'>il</span>
</span>
<span class='ocr_line' id='line_1_8' title="bbox 180 509 646 547; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
<span class='ocrx_word' id='word_1_39' title='bbox 180 512 231 540; x_wconf 96'>est</span>
<span class='ocrx_word' id='word_1_40' title='bbox 245 510 396 547; x_wconf 96'>question</span>
<span class='ocrx_word' id='word_1_41' title='bbox 412 509 492 540; x_wconf 96'>dans</span>
<span class='ocrx_word' id='word_1_42' title='bbox 509 509 531 540; x_wconf 95'>la</span>
<span class='ocrx_word' id='word_1_43' title='bbox 548 519 646 540; x_wconf 96'>cause</span>
</span>
<span class='ocr_line' id='line_1_9' title="bbox 180 559 736 598; baseline 0 -8; x_size 36; x_descenders 5; x_ascenders 10">
<span class='ocrx_word' id='word_1_44' title='bbox 180 559 328 598; x_wconf 87'>ambigu&amp;</span>
<span class='ocrx_word' id='word_1_45' title='bbox 343 559 510 590; x_wconf 91'>entendue</span>
<span class='ocrx_word' id='word_1_46' title='bbox 525 560 541 590; x_wconf 93'>a</span>
<span class='ocrx_word' id='word_1_47' title='bbox 559 560 641 597; x_wconf 61'>MoY,</span>
<span class='ocrx_word' id='word_1_48' title='bbox 657 559 736 590; x_wconf 96'>dans</span>
</span>
<span class='ocr_line' id='line_1_10' title="bbox 181 609 719 647; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
<span class='ocrx_word' id='word_1_49' title='bbox 181 619 221 640; x_wconf 93'>un</span>
<span class='ocrx_word' id='word_1_50' title='bbox 237 609 452 647; x_wconf 92'>capharnaüm</span>
<span class='ocrx_word' id='word_1_51' title='bbox 469 610 528 647; x_wconf 93'>qui,</span>
<span class='ocrx_word' id='word_1_52' title='bbox 545 609 719 647; x_wconf 91'>pense-t-il,</span>
</span>
<span class='ocr_line' id='line_1_11' title="bbox 180 659 760 697; baseline 0 -7; x_size 38; x_descenders 7; x_ascenders 10">
<span class='ocrx_word' id='word_1_53' title='bbox 180 659 319 690; x_wconf 90'>diminue</span>
<span class='ocrx_word' id='word_1_54' title='bbox 334 660 369 697; x_wconf 95'>ca</span>
<span class='ocrx_word' id='word_1_55' title='bbox 385 662 418 690; x_wconf 95'>et</span>
<span class='ocrx_word' id='word_1_56' title='bbox 434 659 457 690; x_wconf 94'>la</span>
<span class='ocrx_word' id='word_1_57' title='bbox 474 659 497 690; x_wconf 92'>la</span>
<span class='ocrx_word' id='word_1_58' title='bbox 514 659 630 697; x_wconf 75'>qualit&amp;</span>
<span class='ocrx_word' id='word_1_59' title='bbox 645 659 686 690; x_wconf 96'>de</span>
<span class='ocrx_word' id='word_1_60' title='bbox 701 669 760 690; x_wconf 96'>son</span>
</span>
<span class='ocr_line' id='line_1_12' title="bbox 180 719 300 740; baseline 0 0; x_size 39.328766; x_descenders 7.3287673; x_ascenders 10.5">
<span class='ocrx_word' id='word_1_61' title='bbox 180 719 300 740; x_wconf 66'>ceuvre.</span>
</span>
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,12 @@
Portez ce vieux whisky au juge
blond qui fume sur son Ile
interieure, a cöte de l'alcöve
ovoide, oU les büches se
consument dans l'ätre, ce qui
ui permet de penser & la
caenogenese de |'etre dont il
est question dans la cause
ambigu& entendue a MoY, dans
un capharnaüm qui, pense-t-il,
diminue ca et la la qualit& de son
ceuvre.
@@ -0,0 +1,190 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
<meta name='ocr-system' content='tesseract 5.3.2' />
<meta name='ocr-capabilities' content='ocr_page ocr_carea ocr_par ocr_line ocrx_word ocrp_wconf'/>
</head>
<body>
<div class='ocr_page' id='page_1' title='image "/tmp/ocrmypdf.io.2ahiyq00/000001_ocr.png"; bbox 0 0 3992 1667; ppageno 0; scan_res 400 400'>
<div class='ocr_separator' id='block_1_1' title="bbox 185 50 3278 54"></div>
<div class='ocr_separator' id='block_1_2' title="bbox 185 249 3278 254"></div>
<div class='ocr_separator' id='block_1_3' title="bbox 185 446 1721 450"></div>
<div class='ocr_separator' id='block_1_4' title="bbox 184 642 846 646"></div>
<div class='ocr_separator' id='block_1_5' title="bbox 184 842 966 846"></div>
<div class='ocr_separator' id='block_1_6' title="bbox 204 50 209 1257"></div>
<div class='ocr_separator' id='block_1_7' title="bbox 2937 446 3278 450"></div>
<div class='ocr_separator' id='block_1_8' title="bbox 1183 642 2438 646"></div>
<div class='ocr_separator' id='block_1_9' title="bbox 2930 642 3278 646"></div>
<div class='ocr_separator' id='block_1_10' title="bbox 1907 842 3278 846"></div>
<div class='ocr_separator' id='block_1_11' title="bbox 448 1037 2904 1042"></div>
<div class='ocr_separator' id='block_1_12' title="bbox 209 1220 1765 1261"></div>
<div class='ocr_separator' id='block_1_13' title="bbox 1725 1133 2047 1151"></div>
<div class='ocr_separator' id='block_1_14' title="bbox 1755 1229 3279 1243"></div>
<div class='ocr_carea' id='block_1_15' title="bbox 45 36 3110 1619">
<p class='ocr_par' id='par_1_1' lang='eng' title="bbox 45 36 3110 1619">
<span class='ocr_line' id='line_1_1' title="bbox 68 36 153 72; baseline 0 0; x_size 43.868851; x_descenders 7.8688521; x_ascenders 11.409836">
<span class='ocrx_word' id='word_1_1' title='bbox 68 36 153 72; x_wconf 95'>600</span>
</span>
<span class='ocr_line' id='line_1_2' title="bbox 69 66 388 446; baseline -0.006 -178; x_size 43.868851; x_descenders 7.8688521; x_ascenders 11.409836">
<span class='ocrx_word' id='word_1_2' title='bbox 69 232 153 268; x_wconf 96'>500</span>
<span class='ocrx_word' id='word_1_3' title='bbox 262 66 388 446; x_wconf 29'>.</span>
</span>
<span class='ocr_line' id='line_1_3' title="bbox 63 388 2969 642; baseline -0.009 -173.963; x_size 62.52; x_descenders 11.52; x_ascenders 15">
<span class='ocrx_word' id='word_1_4' title='bbox 63 413 158 484; x_wconf 96'>400</span>
<span class='ocrx_word' id='word_1_5' title='bbox 355 400 657 642; x_wconf 46'>_</span>
<span class='ocrx_word' id='word_1_6' title='bbox 2164 392 2374 463; x_wconf 28'>EB</span>
<span class='ocrx_word' id='word_1_7' title='bbox 2731 388 2788 458; x_wconf 23'>fp</span>
<span class='ocrx_word' id='word_1_8' title='bbox 2814 450 2969 627; x_wconf 0'>ys</span>
</span>
<span class='ocr_line' id='line_1_4' title="bbox 69 446 2897 771; baseline -0.009 -107; x_size 59; x_descenders 9; x_ascenders 14">
<span class='ocrx_word' id='word_1_9' title='bbox 69 610 153 677; x_wconf 96'>300</span>
<span class='ocrx_word' id='word_1_10' title='bbox 941 618 1130 666; x_wconf 0'>—f—</span>
<span class='ocrx_word' id='word_1_11' title='bbox 1679 446 1950 771; x_wconf 0'>EN</span>
<span class='ocrx_word' id='word_1_12' title='bbox 2031 446 2140 771; x_wconf 0'>/</span>
<span class='ocrx_word' id='word_1_13' title='bbox 2501 446 2706 655; x_wconf 73'>~/</span>
<span class='ocrx_word' id='word_1_14' title='bbox 2793 450 2897 642; x_wconf 19'>Y</span>
</span>
<span class='ocr_line' id='line_1_5' title="bbox 70 646 2737 964; baseline -0.016 -24; x_size 131; x_descenders 32; x_ascenders 39">
<span class='ocrx_word' id='word_1_15' title='bbox 70 828 365 964; x_wconf 0'>Tg</span>
<span class='ocrx_word' id='word_1_16' title='bbox 1494 670 2098 940; x_wconf 42'>ANA</span>
<span class='ocrx_word' id='word_1_17' title='bbox 2158 646 2737 948; x_wconf 40'>a3</span>
</span>
<span class='ocr_line' id='line_1_6' title="bbox 70 1024 2937 1141; baseline -0.002 -56; x_size 31.799999; x_descenders 4.7999997; x_ascenders 12">
<span class='ocrx_word' id='word_1_18' title='bbox 70 1024 152 1061; x_wconf 90'>100</span>
<span class='ocrx_word' id='word_1_19' title='bbox 184 1037 204 1042; x_wconf 50'>-</span>
<span class='ocrx_word' id='word_1_20' title='bbox 1247 1038 1428 1141; x_wconf 23'>nee</span>
<span class='ocrx_word' id='word_1_21' title='bbox 1453 1038 1642 1141; x_wconf 0'>-eemiae</span>
</span>
<span class='ocr_line' id='line_1_7' title="bbox 121 1154 2840 1292; baseline -0.008 -12; x_size 41; x_descenders 8; x_ascenders 12">
<span class='ocrx_word' id='word_1_22' title='bbox 121 1216 299 1292; x_wconf 0'>0-+—-*</span>
<span class='ocrx_word' id='word_1_23' title='bbox 1811 1243 1964 1263; x_wconf 13'>a</span>
<span class='ocrx_word' id='word_1_24' title='bbox 2167 1154 2307 1274; x_wconf 31'>ee</span>
<span class='ocrx_word' id='word_1_25' title='bbox 2377 1154 2477 1274; x_wconf 17'>ee</span>
<span class='ocrx_word' id='word_1_26' title='bbox 2527 1154 2617 1274; x_wconf 32'>ee</span>
<span class='ocrx_word' id='word_1_27' title='bbox 2667 1243 2840 1263; x_wconf 26'>eee</span>
</span>
<span class='ocr_line' id='line_1_8' title="bbox 248 1293 3058 1353; baseline -0.002 -14; x_size 50; x_descenders 11; x_ascenders 8">
<span class='ocrx_word' id='word_1_28' title='bbox 248 1300 279 1339; x_wconf 53'>S</span>
<span class='ocrx_word' id='word_1_29' title='bbox 395 1310 425 1339; x_wconf 49'>@</span>
<span class='ocrx_word' id='word_1_30' title='bbox 540 1300 572 1339; x_wconf 23'>s</span>
<span class='ocrx_word' id='word_1_31' title='bbox 979 1309 1012 1339; x_wconf 81'>&gt;</span>
<span class='ocrx_word' id='word_1_32' title='bbox 1121 1295 1155 1353; x_wconf 37'>oO</span>
<span class='ocrx_word' id='word_1_33' title='bbox 1278 1306 1304 1334; x_wconf 24'>©</span>
<span class='ocrx_word' id='word_1_34' title='bbox 1407 1295 1450 1353; x_wconf 20'>ve</span>
<span class='ocrx_word' id='word_1_35' title='bbox 1567 1308 1593 1338; x_wconf 61'>S</span>
<span class='ocrx_word' id='word_1_36' title='bbox 1712 1308 1740 1338; x_wconf 0'>)</span>
<span class='ocrx_word' id='word_1_37' title='bbox 1858 1308 1888 1338; x_wconf 61'>@</span>
<span class='ocrx_word' id='word_1_38' title='bbox 2004 1305 2037 1338; x_wconf 80'>+</span>
<span class='ocrx_word' id='word_1_39' title='bbox 2153 1308 2180 1336; x_wconf 7'>&amp;</span>
<span class='ocrx_word' id='word_1_40' title='bbox 2287 1305 2329 1347; x_wconf 88'>&amp;</span>
<span class='ocrx_word' id='word_1_41' title='bbox 2578 1293 2613 1351; x_wconf 51'>“A</span>
<span class='ocrx_word' id='word_1_42' title='bbox 2738 1308 2765 1335; x_wconf 41'>5</span>
<span class='ocrx_word' id='word_1_43' title='bbox 3014 1308 3058 1344; x_wconf 23'>oo</span>
</span>
<span class='ocr_line' id='line_1_9' title="bbox 184 1306 3034 1411; baseline -0.008 -16; x_size 63; x_descenders 14; x_ascenders 18">
<span class='ocrx_word' id='word_1_44' title='bbox 184 1327 400 1399; x_wconf 40'>Se</span>
<span class='ocrx_word' id='word_1_45' title='bbox 622 1308 718 1404; x_wconf 53'>°</span>
<span class='ocrx_word' id='word_1_46' title='bbox 774 1330 988 1397; x_wconf 28'>Ps</span>
<span class='ocrx_word' id='word_1_47' title='bbox 1194 1360 1251 1411; x_wconf 19'>as</span>
<span class='ocrx_word' id='word_1_48' title='bbox 1360 1324 1425 1390; x_wconf 33'>ge</span>
<span class='ocrx_word' id='word_1_49' title='bbox 1641 1325 1717 1401; x_wconf 5'>eS</span>
<span class='ocrx_word' id='word_1_50' title='bbox 1801 1328 1869 1393; x_wconf 11'>€</span>
<span class='ocrx_word' id='word_1_51' title='bbox 1941 1325 2017 1392; x_wconf 68'>x</span>
<span class='ocrx_word' id='word_1_52' title='bbox 2077 1326 2161 1406; x_wconf 28'>ro</span>
<span class='ocrx_word' id='word_1_53' title='bbox 2232 1337 2297 1403; x_wconf 43'>NS</span>
<span class='ocrx_word' id='word_1_54' title='bbox 2379 1306 2468 1394; x_wconf 38'>Po</span>
<span class='ocrx_word' id='word_1_55' title='bbox 2535 1323 2597 1391; x_wconf 8'>e?</span>
<span class='ocrx_word' id='word_1_56' title='bbox 2667 1324 2751 1406; x_wconf 66'>&amp;</span>
<span class='ocrx_word' id='word_1_57' title='bbox 2845 1308 2913 1376; x_wconf 28'>s</span>
<span class='ocrx_word' id='word_1_58' title='bbox 2959 1324 3034 1401; x_wconf 48'>AS</span>
</span>
<span class='ocr_line' id='line_1_10' title="bbox 161 1360 3110 1447; baseline -0.003 -5; x_size 64; x_descenders 16; x_ascenders 20">
<span class='ocrx_word' id='word_1_59' title='bbox 161 1382 207 1426; x_wconf 15'>a</span>
<span class='ocrx_word' id='word_1_60' title='bbox 436 1384 494 1430; x_wconf 17'>a</span>
<span class='ocrx_word' id='word_1_61' title='bbox 750 1395 758 1441; x_wconf 17'>©</span>
<span class='ocrx_word' id='word_1_62' title='bbox 1318 1378 1521 1443; x_wconf 61'>FF</span>
<span class='ocrx_word' id='word_1_63' title='bbox 1615 1387 1655 1435; x_wconf 48'>SF</span>
<span class='ocrx_word' id='word_1_64' title='bbox 1753 1385 1813 1434; x_wconf 41'>HY</span>
<span class='ocrx_word' id='word_1_65' title='bbox 1891 1382 1960 1442; x_wconf 40'>HK</span>
<span class='ocrx_word' id='word_1_66' title='bbox 2050 1399 2092 1430; x_wconf 17'>SK</span>
<span class='ocrx_word' id='word_1_67' title='bbox 2187 1388 2251 1441; x_wconf 52'>BM</span>
<span class='ocrx_word' id='word_1_68' title='bbox 2630 1396 2678 1435; x_wconf 0'>ee</span>
<span class='ocrx_word' id='word_1_69' title='bbox 2920 1393 3110 1442; x_wconf 44'>sO</span>
</span>
<span class='ocr_line' id='line_1_11' title="bbox 141 1413 3090 1485; baseline -0.005 -17; x_size 42.720001; x_descenders 6.7199998; x_ascenders 15">
<span class='ocrx_word' id='word_1_70' title='bbox 141 1416 171 1446; x_wconf 35'>e</span>
<span class='ocrx_word' id='word_1_71' title='bbox 266 1438 288 1467; x_wconf 87'>&lt;</span>
<span class='ocrx_word' id='word_1_72' title='bbox 413 1426 448 1477; x_wconf 28'>Na</span>
<span class='ocrx_word' id='word_1_73' title='bbox 569 1429 589 1458; x_wconf 0'></span>
<span class='ocrx_word' id='word_1_74' title='bbox 975 1454 982 1462; x_wconf 8'>:</span>
<span class='ocrx_word' id='word_1_75' title='bbox 1142 1428 1183 1463; x_wconf 23'>&gt;</span>
<span class='ocrx_word' id='word_1_76' title='bbox 1300 1421 1334 1471; x_wconf 10'>cy</span>
<span class='ocrx_word' id='word_1_77' title='bbox 1417 1420 1471 1480; x_wconf 57'>xs</span>
<span class='ocrx_word' id='word_1_78' title='bbox 1566 1424 1625 1476; x_wconf 40'>eS</span>
<span class='ocrx_word' id='word_1_79' title='bbox 1882 1433 1910 1460; x_wconf 59'>@</span>
<span class='ocrx_word' id='word_1_80' title='bbox 2024 1417 2069 1456; x_wconf 59'>Ww</span>
<span class='ocrx_word' id='word_1_81' title='bbox 2878 1413 2934 1485; x_wconf 16'>a)</span>
<span class='ocrx_word' id='word_1_82' title='bbox 3029 1425 3090 1476; x_wconf 33'>Oo</span>
</span>
<span class='ocr_line' id='line_1_12' title="bbox 45 1433 3041 1546; baseline -0.011 -11; x_size 108.376; x_descenders 22; x_ascenders 27.375999">
<span class='ocrx_word' id='word_1_83' title='bbox 45 1433 279 1535; x_wconf 61'>ee</span>
<span class='ocrx_word' id='word_1_84' title='bbox 353 1450 430 1514; x_wconf 27'>S&amp;S</span>
<span class='ocrx_word' id='word_1_85' title='bbox 528 1445 722 1535; x_wconf 55'>FF</span>
<span class='ocrx_word' id='word_1_86' title='bbox 915 1443 1158 1546; x_wconf 49'>SF</span>
<span class='ocrx_word' id='word_1_87' title='bbox 1233 1445 1317 1518; x_wconf 42'>HK</span>
<span class='ocrx_word' id='word_1_88' title='bbox 1386 1462 1427 1518; x_wconf 63'>S</span>
<span class='ocrx_word' id='word_1_89' title='bbox 1833 1453 1889 1502; x_wconf 66'>e</span>
<span class='ocrx_word' id='word_1_90' title='bbox 2004 1440 2046 1476; x_wconf 33'>©</span>
<span class='ocrx_word' id='word_1_91' title='bbox 3006 1457 3041 1492; x_wconf 77'>4</span>
</span>
<span class='ocr_line' id='line_1_13' title="bbox 152 1516 1391 1593; baseline -0.006 -40; x_size 40; x_descenders 7; x_ascenders 13">
<span class='ocrx_word' id='word_1_92' title='bbox 152 1520 214 1575; x_wconf 71'>~</span>
<span class='ocrx_word' id='word_1_93' title='bbox 586 1526 646 1587; x_wconf 85'>&amp;</span>
<span class='ocrx_word' id='word_1_94' title='bbox 1304 1516 1391 1593; x_wconf 0'>e</span>
</span>
<span class='ocr_line' id='line_1_14' title="bbox 247 1567 597 1619; baseline -0.009 0; x_size 55.75; x_descenders 10; x_ascenders 14.5">
<span class='ocrx_word' id='word_1_95' title='bbox 247 1570 297 1619; x_wconf 32'>3</span>
<span class='ocrx_word' id='word_1_96' title='bbox 550 1567 597 1609; x_wconf 43'>x</span>
</span>
</p>
</div>
<div class='ocr_carea' id='block_1_16' title="bbox 3353 611 3919 1060">
<p class='ocr_par' id='par_1_2' lang='eng' title="bbox 3354 611 3660 656">
<span class='ocr_line' id='line_1_15' title="bbox 3354 611 3660 656; baseline -0.013 -5; x_size 46; x_descenders 9; x_ascenders 10">
<span class='ocrx_word' id='word_1_97' title='bbox 3354 611 3660 656; x_wconf 59'>—¢—Support</span>
</span>
</p>
<p class='ocr_par' id='par_1_3' lang='eng' title="bbox 3354 711 3831 960">
<span class='ocr_line' id='line_1_16' title="bbox 3354 711 3831 760; baseline 0 -9; x_size 49; x_descenders 9; x_ascenders 13">
<span class='ocrx_word' id='word_1_98' title='bbox 3354 712 3542 751; x_wconf 0'>—H—</span>
<span class='ocrx_word' id='word_1_99' title='bbox 3480 707 3524 765; x_wconf 90'>No</span>
<span class='ocrx_word' id='word_1_100' title='bbox 3555 711 3773 760; x_wconf 80'>vote[note</span>
<span class='ocrx_word' id='word_1_101' title='bbox 3790 711 3831 760; x_wconf 96'>1]</span>
</span>
<span class='ocr_line' id='line_1_17' title="bbox 3355 815 3652 860; baseline 0.003 -9; x_size 44; x_descenders 8; x_ascenders 9">
<span class='ocrx_word' id='word_1_102' title='bbox 3355 811 3457 864; x_wconf 29'>ir</span>
<span class='ocrx_word' id='word_1_103' title='bbox 3492 811 3652 864; x_wconf 96'>Oppose</span>
</span>
<span class='ocr_line' id='line_1_18' title="bbox 3354 912 3735 960; baseline 0.003 -9; x_size 50; x_descenders 10; x_ascenders 12">
<span class='ocrx_word' id='word_1_104' title='bbox 3354 912 3677 960; x_wconf 0'>—=—Net[note</span>
<span class='ocrx_word' id='word_1_105' title='bbox 3694 912 3735 960; x_wconf 96'>2]</span>
</span>
</p>
<p class='ocr_par' id='par_1_4' lang='eng' title="bbox 3353 1011 3919 1060">
<span class='ocr_line' id='line_1_19' title="bbox 3353 1011 3919 1060; baseline 0.004 -10; x_size 51; x_descenders 10; x_ascenders 13">
<span class='ocrx_word' id='word_1_106' title='bbox 3353 1007 3453 1066; x_wconf 23'>re</span>
<span class='ocrx_word' id='word_1_107' title='bbox 3486 1007 3723 1066; x_wconf 82'>Percentage</span>
<span class='ocrx_word' id='word_1_108' title='bbox 3749 1011 3864 1060; x_wconf 95'>[note</span>
<span class='ocrx_word' id='word_1_109' title='bbox 3877 1011 3919 1060; x_wconf 95'>3]</span>
</span>
</p>
</div>
</div>
</body>
</html>
@@ -0,0 +1,22 @@
600
500 .
400 _ EB fp ys
300 —f— EN / ~/ Y
Tg ANA a3
100 - nee -eemiae
0-+—-* a ee ee ee eee
S @ s > oO © ve S ) @ + & & “A 5 oo
Se ° Ps as ge eS € x ro NS Po e? & s AS
a a © FF SF HY HK SK BM ee sO
e < Na : > cy xs eS @ Ww a) Oo
ee S&S FF SF HK S e © 4
~ & e
3 x
—¢—Support
—H— No vote[note 1]
ir Oppose
—=—Net[note 2]
re Percentage [note 3]

Some files were not shown because too many files have changed in this diff Show More