metafile-render 0.1.0__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. metafile_render-0.1.0/LICENSE +21 -0
  2. metafile_render-0.1.0/MANIFEST.in +1 -0
  3. metafile_render-0.1.0/PKG-INFO +166 -0
  4. metafile_render-0.1.0/README.md +132 -0
  5. metafile_render-0.1.0/pyproject.toml +59 -0
  6. metafile_render-0.1.0/setup.cfg +4 -0
  7. metafile_render-0.1.0/src/metafile_render/__init__.py +25 -0
  8. metafile_render-0.1.0/src/metafile_render/__main__.py +10 -0
  9. metafile_render-0.1.0/src/metafile_render/api.py +37 -0
  10. metafile_render-0.1.0/src/metafile_render/binary.py +77 -0
  11. metafile_render-0.1.0/src/metafile_render/cli.py +97 -0
  12. metafile_render-0.1.0/src/metafile_render/font.py +112 -0
  13. metafile_render-0.1.0/src/metafile_render/geometry.py +409 -0
  14. metafile_render-0.1.0/src/metafile_render/limits.py +42 -0
  15. metafile_render-0.1.0/src/metafile_render/models.py +342 -0
  16. metafile_render-0.1.0/src/metafile_render/parser.py +2017 -0
  17. metafile_render-0.1.0/src/metafile_render/py.typed +0 -0
  18. metafile_render-0.1.0/src/metafile_render/render.py +1148 -0
  19. metafile_render-0.1.0/src/metafile_render.egg-info/PKG-INFO +166 -0
  20. metafile_render-0.1.0/src/metafile_render.egg-info/SOURCES.txt +26 -0
  21. metafile_render-0.1.0/src/metafile_render.egg-info/dependency_links.txt +1 -0
  22. metafile_render-0.1.0/src/metafile_render.egg-info/entry_points.txt +2 -0
  23. metafile_render-0.1.0/src/metafile_render.egg-info/requires.txt +18 -0
  24. metafile_render-0.1.0/src/metafile_render.egg-info/top_level.txt +1 -0
  25. metafile_render-0.1.0/tests/_metafile_test_utils.py +364 -0
  26. metafile_render-0.1.0/tests/test_cli.py +133 -0
  27. metafile_render-0.1.0/tests/test_formats.py +70 -0
  28. metafile_render-0.1.0/tests/test_render.py +780 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Xiaomeng Zhao (myhloli)
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 @@
1
+ include tests/*.py
@@ -0,0 +1,166 @@
1
+ Metadata-Version: 2.4
2
+ Name: metafile-render
3
+ Version: 0.1.0
4
+ Summary: Render WMF and EMF files to SVG, PNG, JPEG, and WebP
5
+ Author: Xiaomeng Zhao (myhloli)
6
+ License-Expression: MIT
7
+ Project-URL: Repository, https://github.com/myhloli/metafile-render
8
+ Project-URL: Issues, https://github.com/myhloli/metafile-render/issues
9
+ Keywords: wmf,emf,metafile,svg,png,jpeg,webp,rendering
10
+ Classifier: Programming Language :: Python :: 3.10
11
+ Classifier: Programming Language :: Python :: 3.11
12
+ Classifier: Programming Language :: Python :: 3.12
13
+ Classifier: Programming Language :: Python :: 3.13
14
+ Classifier: Programming Language :: Python :: 3.14
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Topic :: Multimedia :: Graphics :: Graphics Conversion
17
+ Classifier: Typing :: Typed
18
+ Requires-Python: <3.15,>=3.10
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: pillow>=11.0.0; python_version < "3.14"
22
+ Requires-Dist: pillow>=12.0.0; python_version >= "3.14"
23
+ Requires-Dist: pyclipper<2,>=1.3.0; python_version < "3.14"
24
+ Requires-Dist: pyclipper<2,>=1.4.0; python_version >= "3.14"
25
+ Provides-Extra: test
26
+ Requires-Dist: pytest>=8; extra == "test"
27
+ Requires-Dist: pypptx-with-oxml<2,>=1.0.3; extra == "test"
28
+ Provides-Extra: dev
29
+ Requires-Dist: metafile-render[test]; extra == "dev"
30
+ Requires-Dist: ruff; extra == "dev"
31
+ Requires-Dist: build; extra == "dev"
32
+ Requires-Dist: twine; extra == "dev"
33
+ Dynamic: license-file
34
+
35
+ # Metafile Render
36
+
37
+ Render Windows Metafile (WMF) and Enhanced Metafile (EMF) images to SVG, PNG,
38
+ JPEG, and WebP on Linux, macOS, and Windows.
39
+
40
+ Requires Python 3.10–3.14. Runtime dependencies are Pillow and pyclipper;
41
+ no Office installation, external conversion executable, or Windows GDI is required.
42
+
43
+ ## Installation
44
+
45
+ ```bash
46
+ pip install metafile-render
47
+ ```
48
+
49
+ Python 3.10–3.13 requires Pillow >=11.0.0 and pyclipper >=1.3.0,<2.
50
+ Python 3.14 requires Pillow >=12.0.0 and pyclipper >=1.4.0,<2.
51
+ Existing compatible dependencies can be retained; these ranges do not pin a fresh installation to older releases.
52
+
53
+ ## Python API
54
+
55
+ ```python
56
+ from pathlib import Path
57
+ from metafile_render import MetafileError, render_metafile
58
+
59
+ try:
60
+ result = render_metafile(
61
+ Path("input.emf").read_bytes(),
62
+ output_format="svg",
63
+ dpi=144,
64
+ size_hint=None,
65
+ )
66
+ except MetafileError as error:
67
+ print(error.code, str(error))
68
+ else:
69
+ Path("output.svg").write_bytes(result.data)
70
+ print(result.width, result.height, result.media_type, result.partial)
71
+ for diagnostic in result.diagnostics:
72
+ print(diagnostic.code, diagnostic.message)
73
+ ```
74
+
75
+ `render_metafile(data: bytes, *, output_format="png", dpi=144, size_hint=None)`
76
+ returns a `MetafileRenderResult`. `dpi` must be an integer from 1 through 1200;
77
+ `size_hint`, when supplied, is a pair of positive integer pixel dimensions, useful
78
+ for standard WMF images without physical dimensions. Resource limits may reduce
79
+ the actual canvas size.
80
+
81
+ The result contains `data`, `output_format`, `media_type`, `width`, `height`,
82
+ `source_format`, `emfplus_mode`, `partial`, and a tuple of `diagnostics`.
83
+ Each diagnostic contains a code, level, message, and optional record location.
84
+
85
+ Supported output format strings are `svg`, `png`, `jpeg`, and `webp`.
86
+ PNG preserves transparency; JPEG uses a white background at quality 90.
87
+ WebP preserves transparency and uses lossy quality 90, method 4.
88
+ SVG is self-contained, with embedded images and a PNG fallback in metadata.
89
+ Some raster operations require a raster image wrapped in SVG.
90
+
91
+ Public exports are `render_metafile`, `MetafileOutputFormat`,
92
+ `MetafileRenderResult`, `MetafileDiagnostic`, `MetafileError`,
93
+ `MetafileMalformedError`, `MetafileResourceLimitError`, and `MetafileUnsupportedError`.
94
+ The parser, drawing models, and renderer internals are not a stable public API.
95
+ Invalid API arguments raise `TypeError` or `ValueError`; malformed, unsupported,
96
+ and over-budget images raise the corresponding `MetafileError` subclass.
97
+
98
+ ## Command line
99
+
100
+ ```bash
101
+ metafile-render input.emf -o output.svg
102
+ metafile-render input.wmf -o output.png --dpi 144 --size 800 600
103
+ metafile-render input.emf -o output.webp
104
+ python -m metafile_render input.emf -o output.jpg --force
105
+ metafile-render --version
106
+ ```
107
+
108
+ The output extension selects the format: `.svg`, `.png`, `.jpg`, `.jpeg`, or `.webp`
109
+ (case-insensitive). The output directory must exist. Existing output files are
110
+ preserved unless `--force` is supplied; input and output must be different files.
111
+ Completed outputs are published atomically. Input reads are bounded.
112
+
113
+ Exit codes: `0` for a completed conversion (including partial rendering), `1` for
114
+ conversion or filesystem errors, and `2` for invalid arguments. Diagnostics go to
115
+ stderr. Partial rendering is explicitly reported. Use the Python API to inspect
116
+ individual diagnostic fields.
117
+
118
+ ## Rendering and fonts
119
+
120
+ Placeable and standard WMF, common EMF drawing records, text, DIB images, paths,
121
+ transforms, clipping, and common raster operations are supported. EMF+ Dual uses
122
+ the EMF fallback; EMF+ Only is unsupported. Unhandled records may produce partial
123
+ results with diagnostics. This is not a complete implementation of every GDI record.
124
+
125
+ Font lookup uses installed system fonts and common aliases, then Pillow's default
126
+ font. Install the fonts used by the source document for closer text fidelity;
127
+ glyph coverage and measurements can vary across systems. Font files are not bundled.
128
+ WebP output requires a Pillow build with WebP encoding support, as provided by its
129
+ standard wheels; an unavailable encoder raises `MetafileUnsupportedError`.
130
+
131
+ SVG uses `data-metafile-render="wmf-emf"`, PNG metadata ID
132
+ `metafile-render-raster-fallback`, and local clip IDs `metafile-render-clip-N`.
133
+ The generated-image marker is not authentication of an arbitrary SVG. Consumers
134
+ accepting externally supplied SVG should validate its structure independently.
135
+
136
+ Fixed budgets bound input bytes, record and object counts, nesting, geometry,
137
+ embedded images, and rendering work. Input is limited to 128 MiB; generated SVG
138
+ is limited to 64 MiB; the canvas is limited to 8192 per side and 16 million pixels.
139
+
140
+ ## Development
141
+
142
+ ```bash
143
+ uv venv .venv
144
+ uv pip install --python .venv/bin/python -e ".[dev]"
145
+ .venv/bin/python -m pytest
146
+ .venv/bin/python -m ruff check .
147
+ .venv/bin/python -m ruff format --check .
148
+ .venv/bin/python -m build
149
+ .venv/bin/python -m twine check dist/*
150
+ ```
151
+
152
+ On Windows, the virtual environment interpreter is `.venv\Scripts\python.exe`.
153
+ CI tests Python 3.10–3.14 on Linux, macOS, and Windows, plus minimum dependency combinations.
154
+ Real EMF test images are read from a test-only presentation package dependency.
155
+
156
+ ## Publishing
157
+
158
+ Releases use PyPI Trusted Publishing. Configure the PyPI pending publisher with
159
+ project `metafile-render`, owner `myhloli`, repository `metafile-render`, workflow
160
+ `publish.yml`, and environment `pypi`. Publishing a GitHub Release such as `v0.1.0`
161
+ runs tests, verifies that the tag matches the package version, builds the wheel
162
+ and source distribution, and uploads them through OIDC.
163
+
164
+ ## License
165
+
166
+ MIT. Copyright (c) 2026 Xiaomeng Zhao (myhloli).
@@ -0,0 +1,132 @@
1
+ # Metafile Render
2
+
3
+ Render Windows Metafile (WMF) and Enhanced Metafile (EMF) images to SVG, PNG,
4
+ JPEG, and WebP on Linux, macOS, and Windows.
5
+
6
+ Requires Python 3.10–3.14. Runtime dependencies are Pillow and pyclipper;
7
+ no Office installation, external conversion executable, or Windows GDI is required.
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ pip install metafile-render
13
+ ```
14
+
15
+ Python 3.10–3.13 requires Pillow >=11.0.0 and pyclipper >=1.3.0,<2.
16
+ Python 3.14 requires Pillow >=12.0.0 and pyclipper >=1.4.0,<2.
17
+ Existing compatible dependencies can be retained; these ranges do not pin a fresh installation to older releases.
18
+
19
+ ## Python API
20
+
21
+ ```python
22
+ from pathlib import Path
23
+ from metafile_render import MetafileError, render_metafile
24
+
25
+ try:
26
+ result = render_metafile(
27
+ Path("input.emf").read_bytes(),
28
+ output_format="svg",
29
+ dpi=144,
30
+ size_hint=None,
31
+ )
32
+ except MetafileError as error:
33
+ print(error.code, str(error))
34
+ else:
35
+ Path("output.svg").write_bytes(result.data)
36
+ print(result.width, result.height, result.media_type, result.partial)
37
+ for diagnostic in result.diagnostics:
38
+ print(diagnostic.code, diagnostic.message)
39
+ ```
40
+
41
+ `render_metafile(data: bytes, *, output_format="png", dpi=144, size_hint=None)`
42
+ returns a `MetafileRenderResult`. `dpi` must be an integer from 1 through 1200;
43
+ `size_hint`, when supplied, is a pair of positive integer pixel dimensions, useful
44
+ for standard WMF images without physical dimensions. Resource limits may reduce
45
+ the actual canvas size.
46
+
47
+ The result contains `data`, `output_format`, `media_type`, `width`, `height`,
48
+ `source_format`, `emfplus_mode`, `partial`, and a tuple of `diagnostics`.
49
+ Each diagnostic contains a code, level, message, and optional record location.
50
+
51
+ Supported output format strings are `svg`, `png`, `jpeg`, and `webp`.
52
+ PNG preserves transparency; JPEG uses a white background at quality 90.
53
+ WebP preserves transparency and uses lossy quality 90, method 4.
54
+ SVG is self-contained, with embedded images and a PNG fallback in metadata.
55
+ Some raster operations require a raster image wrapped in SVG.
56
+
57
+ Public exports are `render_metafile`, `MetafileOutputFormat`,
58
+ `MetafileRenderResult`, `MetafileDiagnostic`, `MetafileError`,
59
+ `MetafileMalformedError`, `MetafileResourceLimitError`, and `MetafileUnsupportedError`.
60
+ The parser, drawing models, and renderer internals are not a stable public API.
61
+ Invalid API arguments raise `TypeError` or `ValueError`; malformed, unsupported,
62
+ and over-budget images raise the corresponding `MetafileError` subclass.
63
+
64
+ ## Command line
65
+
66
+ ```bash
67
+ metafile-render input.emf -o output.svg
68
+ metafile-render input.wmf -o output.png --dpi 144 --size 800 600
69
+ metafile-render input.emf -o output.webp
70
+ python -m metafile_render input.emf -o output.jpg --force
71
+ metafile-render --version
72
+ ```
73
+
74
+ The output extension selects the format: `.svg`, `.png`, `.jpg`, `.jpeg`, or `.webp`
75
+ (case-insensitive). The output directory must exist. Existing output files are
76
+ preserved unless `--force` is supplied; input and output must be different files.
77
+ Completed outputs are published atomically. Input reads are bounded.
78
+
79
+ Exit codes: `0` for a completed conversion (including partial rendering), `1` for
80
+ conversion or filesystem errors, and `2` for invalid arguments. Diagnostics go to
81
+ stderr. Partial rendering is explicitly reported. Use the Python API to inspect
82
+ individual diagnostic fields.
83
+
84
+ ## Rendering and fonts
85
+
86
+ Placeable and standard WMF, common EMF drawing records, text, DIB images, paths,
87
+ transforms, clipping, and common raster operations are supported. EMF+ Dual uses
88
+ the EMF fallback; EMF+ Only is unsupported. Unhandled records may produce partial
89
+ results with diagnostics. This is not a complete implementation of every GDI record.
90
+
91
+ Font lookup uses installed system fonts and common aliases, then Pillow's default
92
+ font. Install the fonts used by the source document for closer text fidelity;
93
+ glyph coverage and measurements can vary across systems. Font files are not bundled.
94
+ WebP output requires a Pillow build with WebP encoding support, as provided by its
95
+ standard wheels; an unavailable encoder raises `MetafileUnsupportedError`.
96
+
97
+ SVG uses `data-metafile-render="wmf-emf"`, PNG metadata ID
98
+ `metafile-render-raster-fallback`, and local clip IDs `metafile-render-clip-N`.
99
+ The generated-image marker is not authentication of an arbitrary SVG. Consumers
100
+ accepting externally supplied SVG should validate its structure independently.
101
+
102
+ Fixed budgets bound input bytes, record and object counts, nesting, geometry,
103
+ embedded images, and rendering work. Input is limited to 128 MiB; generated SVG
104
+ is limited to 64 MiB; the canvas is limited to 8192 per side and 16 million pixels.
105
+
106
+ ## Development
107
+
108
+ ```bash
109
+ uv venv .venv
110
+ uv pip install --python .venv/bin/python -e ".[dev]"
111
+ .venv/bin/python -m pytest
112
+ .venv/bin/python -m ruff check .
113
+ .venv/bin/python -m ruff format --check .
114
+ .venv/bin/python -m build
115
+ .venv/bin/python -m twine check dist/*
116
+ ```
117
+
118
+ On Windows, the virtual environment interpreter is `.venv\Scripts\python.exe`.
119
+ CI tests Python 3.10–3.14 on Linux, macOS, and Windows, plus minimum dependency combinations.
120
+ Real EMF test images are read from a test-only presentation package dependency.
121
+
122
+ ## Publishing
123
+
124
+ Releases use PyPI Trusted Publishing. Configure the PyPI pending publisher with
125
+ project `metafile-render`, owner `myhloli`, repository `metafile-render`, workflow
126
+ `publish.yml`, and environment `pypi`. Publishing a GitHub Release such as `v0.1.0`
127
+ runs tests, verifies that the tag matches the package version, builds the wheel
128
+ and source distribution, and uploads them through OIDC.
129
+
130
+ ## License
131
+
132
+ MIT. Copyright (c) 2026 Xiaomeng Zhao (myhloli).
@@ -0,0 +1,59 @@
1
+ [build-system]
2
+ requires = ["setuptools>=77.0.3", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "metafile-render"
7
+ version = "0.1.0"
8
+ description = "Render WMF and EMF files to SVG, PNG, JPEG, and WebP"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ authors = [{name = "Xiaomeng Zhao (myhloli)"}]
13
+ requires-python = ">=3.10,<3.15"
14
+ keywords = ["wmf", "emf", "metafile", "svg", "png", "jpeg", "webp", "rendering"]
15
+ classifiers = [
16
+ "Programming Language :: Python :: 3.10",
17
+ "Programming Language :: Python :: 3.11",
18
+ "Programming Language :: Python :: 3.12",
19
+ "Programming Language :: Python :: 3.13",
20
+ "Programming Language :: Python :: 3.14",
21
+ "Operating System :: OS Independent",
22
+ "Topic :: Multimedia :: Graphics :: Graphics Conversion",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = [
26
+ "pillow>=11.0.0; python_version < '3.14'",
27
+ "pillow>=12.0.0; python_version >= '3.14'",
28
+ "pyclipper>=1.3.0,<2; python_version < '3.14'",
29
+ "pyclipper>=1.4.0,<2; python_version >= '3.14'",
30
+ ]
31
+
32
+ [project.optional-dependencies]
33
+ test = ["pytest>=8", "pypptx-with-oxml>=1.0.3,<2"]
34
+ dev = ["metafile-render[test]", "ruff", "build", "twine"]
35
+
36
+ [project.urls]
37
+ Repository = "https://github.com/myhloli/metafile-render"
38
+ Issues = "https://github.com/myhloli/metafile-render/issues"
39
+
40
+ [project.scripts]
41
+ metafile-render = "metafile_render.cli:main"
42
+
43
+ [tool.setuptools.packages.find]
44
+ where = ["src"]
45
+ namespaces = false
46
+
47
+ [tool.setuptools.package-data]
48
+ metafile_render = ["py.typed"]
49
+
50
+ [tool.pytest.ini_options]
51
+ testpaths = ["tests"]
52
+
53
+ [tool.ruff]
54
+ line-length = 128
55
+ target-version = "py310"
56
+
57
+ [tool.ruff.lint]
58
+ select = ["C", "E", "F", "W", "ANN"]
59
+ ignore = ["C901", "ANN204", "ANN401"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,25 @@
1
+ # Copyright (c) 2026 Xiaomeng Zhao (myhloli)
2
+ # SPDX-License-Identifier: MIT
3
+ """WMF/EMF 的跨平台渲染 API。"""
4
+
5
+ from .api import render_metafile
6
+ from .models import (
7
+ MetafileDiagnostic,
8
+ MetafileError,
9
+ MetafileMalformedError,
10
+ MetafileOutputFormat,
11
+ MetafileRenderResult,
12
+ MetafileResourceLimitError,
13
+ MetafileUnsupportedError,
14
+ )
15
+
16
+ __all__ = [
17
+ "MetafileDiagnostic",
18
+ "MetafileError",
19
+ "MetafileMalformedError",
20
+ "MetafileOutputFormat",
21
+ "MetafileRenderResult",
22
+ "MetafileResourceLimitError",
23
+ "MetafileUnsupportedError",
24
+ "render_metafile",
25
+ ]
@@ -0,0 +1,10 @@
1
+ # Copyright (c) 2026 Xiaomeng Zhao (myhloli)
2
+ # SPDX-License-Identifier: MIT
3
+ """支持 python -m metafile_render。"""
4
+
5
+ from .cli import main
6
+
7
+ __all__: list[str] = []
8
+
9
+ if __name__ == "__main__":
10
+ raise SystemExit(main())
@@ -0,0 +1,37 @@
1
+ # Copyright (c) 2026 Xiaomeng Zhao (myhloli)
2
+ # SPDX-License-Identifier: MIT
3
+ """WMF/EMF 公共渲染入口。"""
4
+
5
+ from __future__ import annotations
6
+
7
+ from .models import MetafileOutputFormat, MetafileRenderResult
8
+ from .parser import parse_metafile
9
+ from .render import encode_document
10
+
11
+
12
+ def render_metafile(
13
+ data: bytes,
14
+ *,
15
+ output_format: MetafileOutputFormat = "png",
16
+ dpi: int = 144,
17
+ size_hint: tuple[int, int] | None = None,
18
+ ) -> MetafileRenderResult:
19
+ """把 WMF/EMF 字节渲染为 PNG、JPEG、WebP 或安全 SVG。"""
20
+ if output_format not in {"png", "jpeg", "svg", "webp"}:
21
+ raise ValueError(f"unsupported metafile output format: {output_format}")
22
+ document = parse_metafile(data, dpi=dpi, size_hint=size_hint)
23
+ output, media_type = encode_document(document, output_format)
24
+ return MetafileRenderResult(
25
+ data=output,
26
+ output_format=output_format,
27
+ media_type=media_type,
28
+ width=document.width,
29
+ height=document.height,
30
+ source_format=document.source_format,
31
+ emfplus_mode=document.emfplus_mode,
32
+ partial=document.partial,
33
+ diagnostics=document.diagnostics,
34
+ )
35
+
36
+
37
+ __all__ = ["render_metafile"]
@@ -0,0 +1,77 @@
1
+ # Copyright (c) 2026 Xiaomeng Zhao (myhloli)
2
+ # SPDX-License-Identifier: MIT
3
+ """WMF/EMF 使用的严格有界二进制读取器。"""
4
+
5
+ from __future__ import annotations
6
+
7
+ import struct
8
+
9
+ from .models import MetafileMalformedError
10
+
11
+
12
+ class BoundedReader:
13
+ """在固定 memoryview 边界内读取小端标量和切片。"""
14
+
15
+ def __init__(self, data: bytes | memoryview, *, base_offset: int = 0) -> None:
16
+ """保存输入视图与用于诊断的绝对起始偏移。"""
17
+ self.data = memoryview(data)
18
+ self.base_offset = base_offset
19
+
20
+ def _require(self, offset: int, size: int) -> None:
21
+ """验证读取范围,不允许负数或越过当前视图。"""
22
+ if offset < 0 or size < 0 or offset > len(self.data) - size:
23
+ raise MetafileMalformedError(
24
+ f"metafile field exceeds record boundary: offset={self.base_offset + offset}, size={size}"
25
+ )
26
+
27
+ def u8(self, offset: int) -> int:
28
+ """读取小端无符号 8 位整数。"""
29
+ self._require(offset, 1)
30
+ return int(self.data[offset])
31
+
32
+ def i16(self, offset: int) -> int:
33
+ """读取小端有符号 16 位整数。"""
34
+ self._require(offset, 2)
35
+ return int(struct.unpack_from("<h", self.data, offset)[0])
36
+
37
+ def u16(self, offset: int) -> int:
38
+ """读取小端无符号 16 位整数。"""
39
+ self._require(offset, 2)
40
+ return int(struct.unpack_from("<H", self.data, offset)[0])
41
+
42
+ def i32(self, offset: int) -> int:
43
+ """读取小端有符号 32 位整数。"""
44
+ self._require(offset, 4)
45
+ return int(struct.unpack_from("<i", self.data, offset)[0])
46
+
47
+ def u32(self, offset: int) -> int:
48
+ """读取小端无符号 32 位整数。"""
49
+ self._require(offset, 4)
50
+ return int(struct.unpack_from("<I", self.data, offset)[0])
51
+
52
+ def f32(self, offset: int) -> float:
53
+ """读取小端 IEEE-754 单精度浮点数。"""
54
+ self._require(offset, 4)
55
+ return float(struct.unpack_from("<f", self.data, offset)[0])
56
+
57
+ def bytes(self, offset: int, size: int) -> bytes:
58
+ """返回经过边界校验的不可变字节切片。"""
59
+ self._require(offset, size)
60
+ return self.data[offset : offset + size].tobytes()
61
+
62
+ def subreader(self, offset: int, size: int) -> BoundedReader:
63
+ """返回继承绝对偏移信息的有界子读取器。"""
64
+ self._require(offset, size)
65
+ return BoundedReader(self.data[offset : offset + size], base_offset=self.base_offset + offset)
66
+
67
+ def remaining(self, offset: int) -> int:
68
+ """返回从指定位置到当前视图末尾的剩余字节数。"""
69
+ self._require(offset, 0)
70
+ return len(self.data) - offset
71
+
72
+ def __len__(self) -> int:
73
+ """返回当前视图的总字节数。"""
74
+ return len(self.data)
75
+
76
+
77
+ __all__ = ["BoundedReader"]
@@ -0,0 +1,97 @@
1
+ # Copyright (c) 2026 Xiaomeng Zhao (myhloli)
2
+ # SPDX-License-Identifier: MIT
3
+ """单文件 WMF/EMF 转换命令行。"""
4
+
5
+ from __future__ import annotations
6
+
7
+ import argparse
8
+ import os
9
+ import sys
10
+ import tempfile
11
+ from importlib.metadata import version
12
+ from pathlib import Path
13
+ from typing import Sequence
14
+
15
+ from .api import render_metafile
16
+ from .limits import MAX_METAFILE_BYTES
17
+ from .models import MetafileError, MetafileOutputFormat, MetafileResourceLimitError
18
+
19
+ _OUTPUT_FORMATS: dict[str, MetafileOutputFormat] = {
20
+ ".png": "png",
21
+ ".jpg": "jpeg",
22
+ ".jpeg": "jpeg",
23
+ ".svg": "svg",
24
+ ".webp": "webp",
25
+ }
26
+
27
+
28
+ def _positive_integer(value: str) -> int:
29
+ """将命令行尺寸解析为正整数,错误交由 argparse 报告。"""
30
+ try:
31
+ number = int(value)
32
+ except ValueError as exc:
33
+ raise argparse.ArgumentTypeError("must be a positive integer") from exc
34
+ if number <= 0:
35
+ raise argparse.ArgumentTypeError("must be a positive integer")
36
+ return number
37
+
38
+
39
+ def _read_input(path: Path) -> bytes:
40
+ """最多读取输入预算加一字节,避免完整载入超大文件。"""
41
+ with path.open("rb") as stream:
42
+ data = stream.read(MAX_METAFILE_BYTES + 1)
43
+ if len(data) > MAX_METAFILE_BYTES:
44
+ raise MetafileResourceLimitError(f"metafile exceeds max_metafile_bytes={MAX_METAFILE_BYTES}")
45
+ return data
46
+
47
+
48
+ def _write_output(path: Path, data: bytes, *, overwrite: bool) -> None:
49
+ """在目标目录原子发布完整文件,并保证默认模式不会覆盖并发创建的文件。"""
50
+ descriptor, filename = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
51
+ temporary = Path(filename)
52
+ try:
53
+ with os.fdopen(descriptor, "wb") as stream:
54
+ stream.write(data)
55
+ if overwrite:
56
+ os.replace(temporary, path)
57
+ else:
58
+ os.link(temporary, path)
59
+ finally:
60
+ temporary.unlink(missing_ok=True)
61
+
62
+
63
+ def main(argv: Sequence[str] | None = None) -> int:
64
+ """解析转换参数并返回稳定退出码,所有运行诊断写入 stderr。"""
65
+ parser = argparse.ArgumentParser(prog="metafile-render", description="Render WMF/EMF to SVG, PNG, JPEG or WebP.")
66
+ parser.add_argument("input", type=Path, help="input WMF or EMF file")
67
+ parser.add_argument("-o", "--output", type=Path, required=True, help="output .svg, .png, .jpg, .jpeg or .webp file")
68
+ parser.add_argument("--dpi", type=_positive_integer, default=144, help="render DPI, 1–1200 (default: 144)")
69
+ parser.add_argument("--size", type=_positive_integer, nargs=2, metavar=("WIDTH", "HEIGHT"), help="pixel size hint")
70
+ parser.add_argument("--force", action="store_true", help="replace an existing output file")
71
+ parser.add_argument("--version", action="version", version=f"%(prog)s {version('metafile-render')}")
72
+ arguments = parser.parse_args(argv)
73
+ output_format = _OUTPUT_FORMATS.get(arguments.output.suffix.lower())
74
+ if output_format is None:
75
+ parser.error("output extension must be .svg, .png, .jpg, .jpeg or .webp")
76
+ if arguments.dpi > 1200:
77
+ parser.error("--dpi must be between 1 and 1200")
78
+ try:
79
+ source, destination = arguments.input, arguments.output
80
+ if source.resolve() == destination.resolve() or destination.exists() and source.samefile(destination):
81
+ parser.error("input and output must refer to different files")
82
+ if (destination.exists() or destination.is_symlink()) and not arguments.force:
83
+ raise FileExistsError(f"output already exists: {destination}; use --force to replace it")
84
+ size_hint = (arguments.size[0], arguments.size[1]) if arguments.size else None
85
+ result = render_metafile(_read_input(source), output_format=output_format, dpi=arguments.dpi, size_hint=size_hint)
86
+ _write_output(destination, result.data, overwrite=arguments.force)
87
+ except (MetafileError, OSError) as exc:
88
+ print(f"metafile-render: {exc}", file=sys.stderr)
89
+ return 1
90
+ if result.partial:
91
+ print("metafile-render: partial rendering", file=sys.stderr)
92
+ for diagnostic in result.diagnostics:
93
+ print(f"metafile-render: {diagnostic.level} [{diagnostic.code}] {diagnostic.message}", file=sys.stderr)
94
+ return 0
95
+
96
+
97
+ __all__ = ["main"]