scanmate-merge 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. scanmate_merge-0.0.1/.gitignore +70 -0
  2. scanmate_merge-0.0.1/LICENSE +21 -0
  3. scanmate_merge-0.0.1/PKG-INFO +133 -0
  4. scanmate_merge-0.0.1/README.md +104 -0
  5. scanmate_merge-0.0.1/project.json +39 -0
  6. scanmate_merge-0.0.1/pyproject.toml +73 -0
  7. scanmate_merge-0.0.1/scanmate_merge/__init__.py +80 -0
  8. scanmate_merge-0.0.1/scanmate_merge/document_merge/__init__.py +19 -0
  9. scanmate_merge-0.0.1/scanmate_merge/document_merge/image_xobject_mapper.py +154 -0
  10. scanmate_merge-0.0.1/scanmate_merge/document_merge/merge_documents_use_case.py +339 -0
  11. scanmate_merge-0.0.1/scanmate_merge/document_merge/merge_result_contract.py +90 -0
  12. scanmate_merge-0.0.1/scanmate_merge/document_merge/test_merge_documents_use_case.py +301 -0
  13. scanmate_merge-0.0.1/scanmate_merge/page_marking/__init__.py +27 -0
  14. scanmate_merge-0.0.1/scanmate_merge/page_marking/mark_pages_use_case.py +288 -0
  15. scanmate_merge-0.0.1/scanmate_merge/page_marking/page_mark_contract.py +71 -0
  16. scanmate_merge-0.0.1/scanmate_merge/page_marking/page_viewport_mapper.py +150 -0
  17. scanmate_merge-0.0.1/scanmate_merge/page_marking/test_mark_pages_use_case.py +200 -0
  18. scanmate_merge-0.0.1/scanmate_merge/page_marking/test_page_viewport_mapper.py +108 -0
  19. scanmate_merge-0.0.1/scanmate_merge/page_placement/__init__.py +21 -0
  20. scanmate_merge-0.0.1/scanmate_merge/page_placement/page_placement_policy.py +111 -0
  21. scanmate_merge-0.0.1/scanmate_merge/page_placement/test_page_placement_policy.py +146 -0
  22. scanmate_merge-0.0.1/scanmate_merge/py.typed +0 -0
  23. scanmate_merge-0.0.1/scanmate_merge/source_reading/__init__.py +15 -0
  24. scanmate_merge-0.0.1/scanmate_merge/source_reading/merge_source_contract.py +22 -0
  25. scanmate_merge-0.0.1/scanmate_merge/source_reading/open_pdf_use_case.py +58 -0
  26. scanmate_merge-0.0.1/scanmate_merge/source_reading/read_source_use_case.py +127 -0
  27. scanmate_merge-0.0.1/scanmate_merge/source_reading/test_read_source_use_case.py +82 -0
  28. scanmate_merge-0.0.1/scanmate_merge/test_package_surface.py +87 -0
@@ -0,0 +1,70 @@
1
+ # See https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
2
+
3
+ # compiled output
4
+ dist
5
+ tmp
6
+ out-tsc
7
+
8
+ # tsc --build writes one of these beside every solution tsconfig.
9
+ *.tsbuildinfo
10
+
11
+ # playground scratch output (aligned.png, diff.png, ...)
12
+ playground-output
13
+
14
+ # dependencies
15
+ node_modules
16
+
17
+ # IDEs and editors
18
+ /.idea
19
+ .project
20
+ .classpath
21
+ .c9/
22
+ *.launch
23
+ .settings/
24
+ *.sublime-workspace
25
+
26
+ # IDE - VSCode
27
+ .vscode/*
28
+ !.vscode/settings.json
29
+ !.vscode/tasks.json
30
+ !.vscode/launch.json
31
+ !.vscode/extensions.json
32
+
33
+ # misc
34
+ .eslintcache
35
+ /.sass-cache
36
+ /connect.lock
37
+ /coverage
38
+ /libpeerconnection.log
39
+ npm-debug.log
40
+ yarn-error.log
41
+ testem.log
42
+ /typings
43
+
44
+ # System Files
45
+ .DS_Store
46
+ Thumbs.db
47
+
48
+ .nx/cache
49
+ .nx/workspace-data
50
+ .cursor/rules/nx-rules.mdc
51
+ .github/instructions/nx.instructions.md
52
+
53
+ .claude/worktrees
54
+ .claude/settings.local.json
55
+ .nx/polygraph
56
+ .nx/self-healing
57
+ .nx/migrate-runs
58
+ vitest.config.*.timestamp*
59
+ # A corpus built from real documents never enters the repository.
60
+ corpus/
61
+ tools/calibration/corpus*/
62
+
63
+ # Python. `mnci add python-lib` does not add these, and the first pytest run
64
+ # writes bytecode and four tool caches straight into the source tree.
65
+ __pycache__/
66
+ *.py[cod]
67
+ .pytest_cache/
68
+ .mypy_cache/
69
+ .ruff_cache/
70
+ *.egg-info/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Eduardo Russo
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,133 @@
1
+ Metadata-Version: 2.5
2
+ Name: scanmate-merge
3
+ Version: 0.0.1
4
+ Summary: Many files in, one PDF out: PDFs, images and rasters, in the order given, mixed freely.
5
+ Project-URL: Homepage, https://github.com/russoedu/scanmate/tree/main/python-packages/scanmate-merge#readme
6
+ Project-URL: Repository, https://github.com/russoedu/scanmate
7
+ Project-URL: Issues, https://github.com/russoedu/scanmate/issues
8
+ Project-URL: Parallels (TypeScript), https://github.com/russoedu/scanmate/tree/main/packages/merge#readme
9
+ Author: Eduardo Russo
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: document,evidence,image,merge,pdf,scan
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Multimedia :: Graphics
22
+ Classifier: Topic :: Text Processing
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.11
25
+ Requires-Dist: numpy>=2.1
26
+ Requires-Dist: pypdf>=5.1
27
+ Requires-Dist: scanmate-ink>=0.0.1
28
+ Description-Content-Type: text/markdown
29
+
30
+ # scanmate-merge
31
+
32
+ Many files in, one PDF out: PDFs, images and rasters, in the order given, mixed
33
+ freely.
34
+
35
+ A **parallel port** of [`@scanmate/merge`][ts], not a replacement for it. The
36
+ TypeScript remains the reference — with one boundary, stated below, that this
37
+ package cannot cross and does not pretend to.
38
+
39
+ ```sh
40
+ pip install scanmate-merge
41
+ ```
42
+
43
+ ```python
44
+ from scanmate_merge import merge_documents
45
+
46
+ result = merge_documents(["scan1.jpg", "contract.pdf", "scan2.png"])
47
+ result.page_count
48
+ result.pages[0].embedding # 'jpeg' - the source's own bytes, not re-compressed
49
+ ```
50
+
51
+ ## What it preserves
52
+
53
+ - **A PDF's pages are copied, not re-rendered**, so their text layer, vectors
54
+ and any signatures stay as they were.
55
+ - **A JPEG is embedded as its own bytes** — RGB or grey, no EXIF rotation
56
+ pending — so a scan is not compressed a second time on its way into evidence.
57
+ - **A PNG's pixels are carried over losslessly.** Not the PNG *file*: a PDF
58
+ cannot hold one, so the pixels go in deflated, which is what the TypeScript
59
+ does too.
60
+ - Anything else is decoded — EXIF rotation applied, transparency flattened onto
61
+ white, every page of a multi-page TIFF — and encoded **once**.
62
+
63
+ **A single PDF on its own comes back byte for byte.** Re-saving a signed PDF
64
+ breaks its signature, and a PDF "merged" alone has nothing to merge, so it is
65
+ not touched at all. Asking for metadata turns that off, because writing metadata
66
+ means writing a new file, and the result says which happened.
67
+
68
+ ## The parity boundary
69
+
70
+ Every other package in this port is held to the TypeScript bit for bit. This one
71
+ cannot be, and the reason is not a shortcut: **two PDF writers lay out objects,
72
+ streams and the cross-reference table differently**, so two valid merges of the
73
+ same sources are never the same file. pdf-lib and `pypdf` are different writers.
74
+
75
+ So parity here is held at the level that actually means something:
76
+
77
+ | Held exactly | Not held |
78
+ | --- | --- |
79
+ | page count, and which source each page came from | the produced PDF's bytes |
80
+ | how each page was embedded (`jpeg`, `png`, `encoded-png`, …) | object numbering, stream order |
81
+ | every page's size in points, and the dpi it was placed at | compression details |
82
+ | whether a single PDF passed through untouched | |
83
+ | page geometry: `place_image`, `resolve_dpi`, the viewport transforms | |
84
+ | `mark_pages`' count and its warning sentences, verbatim | |
85
+
86
+ That is the same boundary [`scanmate-ink`][ink] already draws around
87
+ `resampleRaster` and `blurRaster`: where two engines cannot agree by
88
+ construction, say so rather than write a golden nobody can meet.
89
+
90
+ What the produced file *is* checked for: that it is a readable PDF, that its
91
+ pages are the sizes the result reports, that a JPEG went in as `/DCTDecode` and
92
+ a PNG's pixels as `/FlateDecode`, and that marking a page adds to its content
93
+ rather than replacing it.
94
+
95
+ ## Marking a page
96
+
97
+ `mark_pages` draws each expected field on the original, with its bleed around
98
+ it — a way to see whether the positions a validation will use are where the
99
+ fields actually are, before anything is measured with them.
100
+
101
+ ```python
102
+ from scanmate_merge import PageMark, mark_pages
103
+
104
+ result = mark_pages(pdf, [PageMark(page=1, x=72, y=520, width=180, height=36, id="signature")])
105
+ result.warnings # a mark on a page that does not exist, or one past the edge
106
+ ```
107
+
108
+ Drawn as vectors on the document itself, so it stays sharp at any zoom. The
109
+ coordinates are measured from the **top-left of the page as displayed**, with
110
+ `/Rotate` and the crop box applied — the same frame `scanmate-extract` reports
111
+ text in, so a list measured there can be drawn here unchanged. Getting between
112
+ that frame and the PDF's own is pdf.js's `PageViewport` transform, ported line
113
+ for line and inverted; it is exact, and it is where a "close enough" port would
114
+ give false confidence on exactly the rotated and cropped pages that most need
115
+ checking.
116
+
117
+ ## Two departures from the TypeScript
118
+
119
+ **Nothing is `async`.** `mergeDocuments`, `markPages` and `readSource` are, for
120
+ file reads and libvips; this reads files synchronously, as the rest of the
121
+ Python port does.
122
+
123
+ **`MergeOptions` and `MarkOptions` are dataclasses**, not object literals, so
124
+ `page_size`, `image_dpi` and `pass_through` are snake_case keyword arguments.
125
+
126
+ ## Requirements
127
+
128
+ Python 3.11 or newer, and `scanmate-ink`. Fully typed (PEP 561), `mypy --strict`
129
+ clean. PDF reading and writing is [`pypdf`][pypdf].
130
+
131
+ [ts]: https://github.com/russoedu/scanmate/tree/main/packages/merge#readme
132
+ [ink]: https://github.com/russoedu/scanmate/tree/main/python-packages/scanmate-ink#readme
133
+ [pypdf]: https://pypdf.readthedocs.io/
@@ -0,0 +1,104 @@
1
+ # scanmate-merge
2
+
3
+ Many files in, one PDF out: PDFs, images and rasters, in the order given, mixed
4
+ freely.
5
+
6
+ A **parallel port** of [`@scanmate/merge`][ts], not a replacement for it. The
7
+ TypeScript remains the reference — with one boundary, stated below, that this
8
+ package cannot cross and does not pretend to.
9
+
10
+ ```sh
11
+ pip install scanmate-merge
12
+ ```
13
+
14
+ ```python
15
+ from scanmate_merge import merge_documents
16
+
17
+ result = merge_documents(["scan1.jpg", "contract.pdf", "scan2.png"])
18
+ result.page_count
19
+ result.pages[0].embedding # 'jpeg' - the source's own bytes, not re-compressed
20
+ ```
21
+
22
+ ## What it preserves
23
+
24
+ - **A PDF's pages are copied, not re-rendered**, so their text layer, vectors
25
+ and any signatures stay as they were.
26
+ - **A JPEG is embedded as its own bytes** — RGB or grey, no EXIF rotation
27
+ pending — so a scan is not compressed a second time on its way into evidence.
28
+ - **A PNG's pixels are carried over losslessly.** Not the PNG *file*: a PDF
29
+ cannot hold one, so the pixels go in deflated, which is what the TypeScript
30
+ does too.
31
+ - Anything else is decoded — EXIF rotation applied, transparency flattened onto
32
+ white, every page of a multi-page TIFF — and encoded **once**.
33
+
34
+ **A single PDF on its own comes back byte for byte.** Re-saving a signed PDF
35
+ breaks its signature, and a PDF "merged" alone has nothing to merge, so it is
36
+ not touched at all. Asking for metadata turns that off, because writing metadata
37
+ means writing a new file, and the result says which happened.
38
+
39
+ ## The parity boundary
40
+
41
+ Every other package in this port is held to the TypeScript bit for bit. This one
42
+ cannot be, and the reason is not a shortcut: **two PDF writers lay out objects,
43
+ streams and the cross-reference table differently**, so two valid merges of the
44
+ same sources are never the same file. pdf-lib and `pypdf` are different writers.
45
+
46
+ So parity here is held at the level that actually means something:
47
+
48
+ | Held exactly | Not held |
49
+ | --- | --- |
50
+ | page count, and which source each page came from | the produced PDF's bytes |
51
+ | how each page was embedded (`jpeg`, `png`, `encoded-png`, …) | object numbering, stream order |
52
+ | every page's size in points, and the dpi it was placed at | compression details |
53
+ | whether a single PDF passed through untouched | |
54
+ | page geometry: `place_image`, `resolve_dpi`, the viewport transforms | |
55
+ | `mark_pages`' count and its warning sentences, verbatim | |
56
+
57
+ That is the same boundary [`scanmate-ink`][ink] already draws around
58
+ `resampleRaster` and `blurRaster`: where two engines cannot agree by
59
+ construction, say so rather than write a golden nobody can meet.
60
+
61
+ What the produced file *is* checked for: that it is a readable PDF, that its
62
+ pages are the sizes the result reports, that a JPEG went in as `/DCTDecode` and
63
+ a PNG's pixels as `/FlateDecode`, and that marking a page adds to its content
64
+ rather than replacing it.
65
+
66
+ ## Marking a page
67
+
68
+ `mark_pages` draws each expected field on the original, with its bleed around
69
+ it — a way to see whether the positions a validation will use are where the
70
+ fields actually are, before anything is measured with them.
71
+
72
+ ```python
73
+ from scanmate_merge import PageMark, mark_pages
74
+
75
+ result = mark_pages(pdf, [PageMark(page=1, x=72, y=520, width=180, height=36, id="signature")])
76
+ result.warnings # a mark on a page that does not exist, or one past the edge
77
+ ```
78
+
79
+ Drawn as vectors on the document itself, so it stays sharp at any zoom. The
80
+ coordinates are measured from the **top-left of the page as displayed**, with
81
+ `/Rotate` and the crop box applied — the same frame `scanmate-extract` reports
82
+ text in, so a list measured there can be drawn here unchanged. Getting between
83
+ that frame and the PDF's own is pdf.js's `PageViewport` transform, ported line
84
+ for line and inverted; it is exact, and it is where a "close enough" port would
85
+ give false confidence on exactly the rotated and cropped pages that most need
86
+ checking.
87
+
88
+ ## Two departures from the TypeScript
89
+
90
+ **Nothing is `async`.** `mergeDocuments`, `markPages` and `readSource` are, for
91
+ file reads and libvips; this reads files synchronously, as the rest of the
92
+ Python port does.
93
+
94
+ **`MergeOptions` and `MarkOptions` are dataclasses**, not object literals, so
95
+ `page_size`, `image_dpi` and `pass_through` are snake_case keyword arguments.
96
+
97
+ ## Requirements
98
+
99
+ Python 3.11 or newer, and `scanmate-ink`. Fully typed (PEP 561), `mypy --strict`
100
+ clean. PDF reading and writing is [`pypdf`][pypdf].
101
+
102
+ [ts]: https://github.com/russoedu/scanmate/tree/main/packages/merge#readme
103
+ [ink]: https://github.com/russoedu/scanmate/tree/main/python-packages/scanmate-ink#readme
104
+ [pypdf]: https://pypdf.readthedocs.io/
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "scanmate-merge",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "projectType": "library",
5
+ "sourceRoot": "python-packages/scanmate-merge",
6
+ "targets": {
7
+ "lint": {
8
+ "executor": "@mnci/nx-python-pip:lint",
9
+ "options": {}
10
+ },
11
+ "typecheck": {
12
+ "executor": "@mnci/nx-python-pip:typecheck",
13
+ "options": {}
14
+ },
15
+ "test": {
16
+ "executor": "@mnci/nx-python-pip:test",
17
+ "options": {}
18
+ },
19
+ "build": {
20
+ "executor": "@mnci/nx-python-pip:build",
21
+ "outputs": [
22
+ "{projectRoot}/dist"
23
+ ],
24
+ "options": {}
25
+ },
26
+ "nx-release-publish": {
27
+ "executor": "@mnci/nx-python-pip:publish",
28
+ "dependsOn": [
29
+ "build"
30
+ ],
31
+ "options": {}
32
+ }
33
+ },
34
+ "release": {
35
+ "version": {
36
+ "versionActions": "@mnci/nx-python-pip/release/version-actions"
37
+ }
38
+ }
39
+ }
@@ -0,0 +1,73 @@
1
+ [project]
2
+ name = "scanmate-merge"
3
+ version = "0.0.1"
4
+ description = "Many files in, one PDF out: PDFs, images and rasters, in the order given, mixed freely."
5
+ keywords = ["pdf", "merge", "scan", "document", "image", "evidence"]
6
+ license = "MIT"
7
+ # A COPY of the repository's LICENSE, not a path to it. A Python distribution
8
+ # cannot reference a file outside its own root - `../../LICENSE` builds a sdist
9
+ # whose member escapes the archive, and `python -m build` refuses it outright.
10
+ license-files = ["LICENSE"]
11
+ authors = [{ name = "Eduardo Russo" }]
12
+ readme = "README.md"
13
+ classifiers = [
14
+ "Development Status :: 4 - Beta",
15
+ "Intended Audience :: Developers",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.11",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ "Topic :: Multimedia :: Graphics",
23
+ "Topic :: Text Processing",
24
+ "Typing :: Typed",
25
+ ]
26
+ requires-python = ">=3.11"
27
+ dependencies = [
28
+ "numpy>=2.1",
29
+ "pypdf>=5.1",
30
+ # >=0.0.1, not the real floor, and deliberately: a release never commits its
31
+ # version bump, so every manifest on disk stays at the scaffold version
32
+ # forever, and a real floor here makes the workspace-wide editable install
33
+ # unresolvable. `nx release` rewrites this to the true version as it
34
+ # publishes - "scanmate-ink>=0.0.1" becomes "scanmate-ink>=0.24.1" in the
35
+ # wheel. The same declaration scanmate-align carries.
36
+ "scanmate-ink>=0.0.1",
37
+ ]
38
+
39
+ [project.urls]
40
+ Homepage = "https://github.com/russoedu/scanmate/tree/main/python-packages/scanmate-merge#readme"
41
+ Repository = "https://github.com/russoedu/scanmate"
42
+ Issues = "https://github.com/russoedu/scanmate/issues"
43
+ "Parallels (TypeScript)" = "https://github.com/russoedu/scanmate/tree/main/packages/merge#readme"
44
+
45
+ [build-system]
46
+ requires = ["hatchling"]
47
+ build-backend = "hatchling.build"
48
+
49
+ [tool.hatch.build.targets.wheel]
50
+ packages = ["scanmate_merge"]
51
+ # Tests sit beside the code, so they must be kept OUT of the wheel.
52
+ exclude = ["**/test_*.py", "**/conftest.py"]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["scanmate_merge"]
56
+
57
+ [tool.mypy]
58
+ strict = true
59
+ # Stub-less third-party packages, and ONLY those. Measured on a generated
60
+ # project: plain strict passes out of the box, and the one thing that breaks it
61
+ # in practice is importing a library that ships no type stubs - "import yaml"
62
+ # fails as import-untyped on code you wrote normally and cannot fix. Disabling
63
+ # that one code keeps every other strict check on your own code, and a module
64
+ # that genuinely cannot be found still fails, as import-not-found.
65
+ disable_error_code = ["import-untyped"]
66
+
67
+ [tool.ruff]
68
+ line-length = 100
69
+ target-version = "py311"
70
+
71
+ [tool.ruff.lint]
72
+ # Pyflakes, pycodestyle, isort, bugbear, comprehensions, pyupgrade, naming.
73
+ select = ["F", "E", "W", "I", "B", "C4", "UP", "N"]
@@ -0,0 +1,80 @@
1
+ """``scanmate-merge`` - many files in, one PDF out.
2
+
3
+ .. code-block:: python
4
+
5
+ from scanmate_merge import merge_documents
6
+
7
+ result = merge_documents(["scan1.jpg", "contract.pdf", "scan2.png"])
8
+ result.page_count
9
+ result.pages[0].embedding # 'jpeg', embedded as its own bytes
10
+ """
11
+
12
+ from .document_merge import (
13
+ Embedding,
14
+ MergedPage,
15
+ MergeMetadata,
16
+ MergeOptions,
17
+ MergeResult,
18
+ merge_documents,
19
+ )
20
+ from .page_marking import (
21
+ Affine,
22
+ MarkOptions,
23
+ MarkResult,
24
+ PageGeometry,
25
+ PageMark,
26
+ mark_pages,
27
+ to_user_space,
28
+ viewport_size,
29
+ viewport_transform,
30
+ )
31
+ from .page_placement import (
32
+ MIN_RECORDED_DPI,
33
+ PAPER,
34
+ PageDimensions,
35
+ PageSize,
36
+ Placement,
37
+ place_image,
38
+ resolve_dpi,
39
+ )
40
+ from .source_reading import (
41
+ MergeSourceError,
42
+ PdfPasswordError,
43
+ ResolvedSource,
44
+ SourceKind,
45
+ is_pdf,
46
+ open_pdf,
47
+ read_source,
48
+ )
49
+
50
+ __all__ = [
51
+ "MIN_RECORDED_DPI",
52
+ "PAPER",
53
+ "Affine",
54
+ "Embedding",
55
+ "MarkOptions",
56
+ "MarkResult",
57
+ "MergeMetadata",
58
+ "MergeOptions",
59
+ "MergeResult",
60
+ "MergeSourceError",
61
+ "MergedPage",
62
+ "PageDimensions",
63
+ "PageGeometry",
64
+ "PageMark",
65
+ "PageSize",
66
+ "PdfPasswordError",
67
+ "Placement",
68
+ "ResolvedSource",
69
+ "SourceKind",
70
+ "is_pdf",
71
+ "mark_pages",
72
+ "merge_documents",
73
+ "open_pdf",
74
+ "place_image",
75
+ "read_source",
76
+ "resolve_dpi",
77
+ "to_user_space",
78
+ "viewport_size",
79
+ "viewport_transform",
80
+ ]
@@ -0,0 +1,19 @@
1
+ """Many files in, one PDF out."""
2
+
3
+ from .merge_documents_use_case import merge_documents
4
+ from .merge_result_contract import (
5
+ Embedding,
6
+ MergedPage,
7
+ MergeMetadata,
8
+ MergeOptions,
9
+ MergeResult,
10
+ )
11
+
12
+ __all__ = [
13
+ "Embedding",
14
+ "MergeMetadata",
15
+ "MergeOptions",
16
+ "MergeResult",
17
+ "MergedPage",
18
+ "merge_documents",
19
+ ]