infographics-builder 2.0.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.
- infographics_builder-2.0.0/LICENSE +21 -0
- infographics_builder-2.0.0/PKG-INFO +156 -0
- infographics_builder-2.0.0/README.md +129 -0
- infographics_builder-2.0.0/infographics_builder/__init__.py +17 -0
- infographics_builder-2.0.0/infographics_builder/cli.py +155 -0
- infographics_builder-2.0.0/infographics_builder/processor.py +226 -0
- infographics_builder-2.0.0/infographics_builder/renderer.py +185 -0
- infographics_builder-2.0.0/infographics_builder/validator.py +59 -0
- infographics_builder-2.0.0/infographics_builder.egg-info/PKG-INFO +156 -0
- infographics_builder-2.0.0/infographics_builder.egg-info/SOURCES.txt +14 -0
- infographics_builder-2.0.0/infographics_builder.egg-info/dependency_links.txt +1 -0
- infographics_builder-2.0.0/infographics_builder.egg-info/entry_points.txt +2 -0
- infographics_builder-2.0.0/infographics_builder.egg-info/requires.txt +5 -0
- infographics_builder-2.0.0/infographics_builder.egg-info/top_level.txt +1 -0
- infographics_builder-2.0.0/pyproject.toml +43 -0
- infographics_builder-2.0.0/setup.cfg +4 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 infographics-builder contributors
|
|
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,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infographics-builder
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: CLI library for automated infographic poster generation: background removal, parallel headless rendering, color-based auto-patching and HTML asset validation.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/yourusername/infographics-builder
|
|
7
|
+
Project-URL: Issues, https://github.com/yourusername/infographics-builder/issues
|
|
8
|
+
Keywords: infographics,poster,rendering,rembg,pillow,background-removal,headless-chrome
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: Pillow>=10.0.0
|
|
23
|
+
Requires-Dist: rembg>=2.0.0
|
|
24
|
+
Provides-Extra: progress
|
|
25
|
+
Requires-Dist: tqdm>=4.60.0; extra == "progress"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# infographics-builder
|
|
29
|
+
|
|
30
|
+
> CLI Python library for automated generation of high-quality infographic posters.
|
|
31
|
+
> Background removal • Parallel headless rendering • BFS color auto-patching • Asset validation
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
| Command | Description |
|
|
38
|
+
|---------|-------------|
|
|
39
|
+
| `bg` | Remove background via `rembg` and center object on transparent canvas |
|
|
40
|
+
| `render` | Render HTML template to print-quality JPG (2400×3600px) via headless Brave/Chrome |
|
|
41
|
+
| `batch` | Parallel batch rendering of entire folder with progress bar and build log |
|
|
42
|
+
| `patch` | Surgical rectangular texture patching via linear color interpolation |
|
|
43
|
+
| `autopatch` | BFS-based automatic color region detection and patching (removes flags, labels) |
|
|
44
|
+
| `validate` | Pre-render validation of all local image assets in HTML template |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install infographics-builder
|
|
52
|
+
|
|
53
|
+
# Optional: progress bar support
|
|
54
|
+
pip install infographics-builder[progress]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Requirements:**
|
|
58
|
+
- Python 3.10+
|
|
59
|
+
- Brave Browser or Google Chrome (for `render` / `batch` commands)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### Remove background from image
|
|
66
|
+
```bash
|
|
67
|
+
infographics-builder bg input_photo.jpg output.png
|
|
68
|
+
infographics-builder bg input_photo.jpg output.png --size 1024 --padding 40
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Render a single poster
|
|
72
|
+
```bash
|
|
73
|
+
infographics-builder render card.html poster.jpg
|
|
74
|
+
infographics-builder render card.html poster.jpg --browser "C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Batch render entire folder (with skip-existing and auto log)
|
|
78
|
+
```bash
|
|
79
|
+
infographics-builder batch ./html_pages/ ./renders/
|
|
80
|
+
infographics-builder batch ./html_pages/ ./renders/ --workers 4 --no-skip
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Auto-patch unwanted color regions (flags, watermarks)
|
|
84
|
+
```bash
|
|
85
|
+
# Remove red (255,0,0) with tolerance 40
|
|
86
|
+
infographics-builder autopatch model.png 255 0 0 --tolerance 40
|
|
87
|
+
|
|
88
|
+
# Remove blue flag without creating a backup
|
|
89
|
+
infographics-builder autopatch model.png 0 0 200 --tolerance 30 --no-backup
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Manual texture patch by coordinates
|
|
93
|
+
```bash
|
|
94
|
+
infographics-builder patch model.png 580 620 340 380
|
|
95
|
+
infographics-builder patch model.png 580 620 340 380 --no-backup
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Validate HTML template assets
|
|
99
|
+
```bash
|
|
100
|
+
infographics-builder validate card.html
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Python API
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from infographics_builder import ImageProcessor, PosterRenderer, Validator
|
|
109
|
+
|
|
110
|
+
# Remove background
|
|
111
|
+
ImageProcessor.remove_background("photo.jpg", "output.png")
|
|
112
|
+
|
|
113
|
+
# Render poster
|
|
114
|
+
renderer = PosterRenderer() # auto-detects Brave/Chrome
|
|
115
|
+
renderer.render("card.html", "poster.jpg")
|
|
116
|
+
|
|
117
|
+
# Batch render with progress bar
|
|
118
|
+
renderer.render_batch(
|
|
119
|
+
html_paths=["card1.html", "card2.html"],
|
|
120
|
+
output_jpg_paths=["poster1.jpg", "poster2.jpg"],
|
|
121
|
+
skip_existing=True, # skip already rendered
|
|
122
|
+
log_dir="./logs/" # write build_YYYY-MM-DD.log
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Auto-patch by color (BFS)
|
|
126
|
+
ImageProcessor.auto_patch_by_color("model.png", target_color_rgb=(255, 0, 0), tolerance=30)
|
|
127
|
+
|
|
128
|
+
# Validate assets
|
|
129
|
+
ok = Validator.validate_html("card.html")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Rendering Architecture
|
|
135
|
+
|
|
136
|
+
The `render` command uses a Chromium-based headless browser with these flags:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
--window-size=1200,1800 --force-device-scale-factor=2
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This produces a **2400×3600px** screenshot (device-pixel-ratio 2x), which is then converted to JPG at 95% quality via Pillow.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Safety Features
|
|
147
|
+
|
|
148
|
+
- **Auto-backup (`.bak`)**: `patch` and `autopatch` create a `.bak` copy before modifying any file. Disable with `--no-backup`.
|
|
149
|
+
- **Skip existing**: `batch` skips already-rendered JPGs by default (`--no-skip` to override).
|
|
150
|
+
- **Build log**: Every `batch` run writes `build_YYYY-MM-DD.log` in the output directory.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
# infographics-builder
|
|
2
|
+
|
|
3
|
+
> CLI Python library for automated generation of high-quality infographic posters.
|
|
4
|
+
> Background removal • Parallel headless rendering • BFS color auto-patching • Asset validation
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## Features
|
|
9
|
+
|
|
10
|
+
| Command | Description |
|
|
11
|
+
|---------|-------------|
|
|
12
|
+
| `bg` | Remove background via `rembg` and center object on transparent canvas |
|
|
13
|
+
| `render` | Render HTML template to print-quality JPG (2400×3600px) via headless Brave/Chrome |
|
|
14
|
+
| `batch` | Parallel batch rendering of entire folder with progress bar and build log |
|
|
15
|
+
| `patch` | Surgical rectangular texture patching via linear color interpolation |
|
|
16
|
+
| `autopatch` | BFS-based automatic color region detection and patching (removes flags, labels) |
|
|
17
|
+
| `validate` | Pre-render validation of all local image assets in HTML template |
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## Installation
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install infographics-builder
|
|
25
|
+
|
|
26
|
+
# Optional: progress bar support
|
|
27
|
+
pip install infographics-builder[progress]
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Requirements:**
|
|
31
|
+
- Python 3.10+
|
|
32
|
+
- Brave Browser or Google Chrome (for `render` / `batch` commands)
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Quick Start
|
|
37
|
+
|
|
38
|
+
### Remove background from image
|
|
39
|
+
```bash
|
|
40
|
+
infographics-builder bg input_photo.jpg output.png
|
|
41
|
+
infographics-builder bg input_photo.jpg output.png --size 1024 --padding 40
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Render a single poster
|
|
45
|
+
```bash
|
|
46
|
+
infographics-builder render card.html poster.jpg
|
|
47
|
+
infographics-builder render card.html poster.jpg --browser "C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Batch render entire folder (with skip-existing and auto log)
|
|
51
|
+
```bash
|
|
52
|
+
infographics-builder batch ./html_pages/ ./renders/
|
|
53
|
+
infographics-builder batch ./html_pages/ ./renders/ --workers 4 --no-skip
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### Auto-patch unwanted color regions (flags, watermarks)
|
|
57
|
+
```bash
|
|
58
|
+
# Remove red (255,0,0) with tolerance 40
|
|
59
|
+
infographics-builder autopatch model.png 255 0 0 --tolerance 40
|
|
60
|
+
|
|
61
|
+
# Remove blue flag without creating a backup
|
|
62
|
+
infographics-builder autopatch model.png 0 0 200 --tolerance 30 --no-backup
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Manual texture patch by coordinates
|
|
66
|
+
```bash
|
|
67
|
+
infographics-builder patch model.png 580 620 340 380
|
|
68
|
+
infographics-builder patch model.png 580 620 340 380 --no-backup
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Validate HTML template assets
|
|
72
|
+
```bash
|
|
73
|
+
infographics-builder validate card.html
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## Python API
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from infographics_builder import ImageProcessor, PosterRenderer, Validator
|
|
82
|
+
|
|
83
|
+
# Remove background
|
|
84
|
+
ImageProcessor.remove_background("photo.jpg", "output.png")
|
|
85
|
+
|
|
86
|
+
# Render poster
|
|
87
|
+
renderer = PosterRenderer() # auto-detects Brave/Chrome
|
|
88
|
+
renderer.render("card.html", "poster.jpg")
|
|
89
|
+
|
|
90
|
+
# Batch render with progress bar
|
|
91
|
+
renderer.render_batch(
|
|
92
|
+
html_paths=["card1.html", "card2.html"],
|
|
93
|
+
output_jpg_paths=["poster1.jpg", "poster2.jpg"],
|
|
94
|
+
skip_existing=True, # skip already rendered
|
|
95
|
+
log_dir="./logs/" # write build_YYYY-MM-DD.log
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
# Auto-patch by color (BFS)
|
|
99
|
+
ImageProcessor.auto_patch_by_color("model.png", target_color_rgb=(255, 0, 0), tolerance=30)
|
|
100
|
+
|
|
101
|
+
# Validate assets
|
|
102
|
+
ok = Validator.validate_html("card.html")
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Rendering Architecture
|
|
108
|
+
|
|
109
|
+
The `render` command uses a Chromium-based headless browser with these flags:
|
|
110
|
+
|
|
111
|
+
```
|
|
112
|
+
--window-size=1200,1800 --force-device-scale-factor=2
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
This produces a **2400×3600px** screenshot (device-pixel-ratio 2x), which is then converted to JPG at 95% quality via Pillow.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Safety Features
|
|
120
|
+
|
|
121
|
+
- **Auto-backup (`.bak`)**: `patch` and `autopatch` create a `.bak` copy before modifying any file. Disable with `--no-backup`.
|
|
122
|
+
- **Skip existing**: `batch` skips already-rendered JPGs by default (`--no-skip` to override).
|
|
123
|
+
- **Build log**: Every `batch` run writes `build_YYYY-MM-DD.log` in the output directory.
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## License
|
|
128
|
+
|
|
129
|
+
MIT
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infographics-builder
|
|
3
|
+
====================
|
|
4
|
+
CLI library for automated infographic poster generation.
|
|
5
|
+
|
|
6
|
+
Provides:
|
|
7
|
+
- ImageProcessor: background removal, texture patching, BFS color auto-patch
|
|
8
|
+
- PosterRenderer: headless HTML -> JPG rendering (single & batch)
|
|
9
|
+
- Validator: pre-render HTML asset validation
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "2.0.0"
|
|
13
|
+
__all__ = ["ImageProcessor", "PosterRenderer", "Validator"]
|
|
14
|
+
|
|
15
|
+
from infographics_builder.processor import ImageProcessor
|
|
16
|
+
from infographics_builder.renderer import PosterRenderer
|
|
17
|
+
from infographics_builder.validator import Validator
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"""
|
|
2
|
+
infographics-builder CLI entry point.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import logging
|
|
8
|
+
import argparse
|
|
9
|
+
|
|
10
|
+
from infographics_builder.processor import ImageProcessor
|
|
11
|
+
from infographics_builder.renderer import PosterRenderer
|
|
12
|
+
from infographics_builder.validator import Validator
|
|
13
|
+
|
|
14
|
+
# Setup logging
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO,
|
|
17
|
+
format="[%(levelname)s] %(message)s",
|
|
18
|
+
stream=sys.stdout,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_DESCRIPTION = """
|
|
22
|
+
infographics-builder v2.0.0 — Automated infographic poster generation.
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
bg Remove background and center image on transparent canvas
|
|
26
|
+
render Render HTML template to high-res JPG poster (headless browser)
|
|
27
|
+
batch Parallel batch rendering of entire folder with progress + log
|
|
28
|
+
patch Surgical rectangular texture patching by coordinates
|
|
29
|
+
autopatch BFS auto-patch by color (flags, watermarks, labels)
|
|
30
|
+
validate Validate all local image assets in HTML template
|
|
31
|
+
|
|
32
|
+
Examples:
|
|
33
|
+
infographics-builder bg photo.jpg output.png
|
|
34
|
+
infographics-builder render card.html poster.jpg
|
|
35
|
+
infographics-builder batch ./html_pages/ ./renders/
|
|
36
|
+
infographics-builder autopatch model.png 255 0 0 --tolerance 40
|
|
37
|
+
infographics-builder validate card.html
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> None:
|
|
42
|
+
parser = argparse.ArgumentParser(
|
|
43
|
+
prog="infographics-builder",
|
|
44
|
+
description="infographics-builder v2.0.0 — Automated infographic poster generation",
|
|
45
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
46
|
+
epilog="Per-command help: infographics-builder <command> --help",
|
|
47
|
+
)
|
|
48
|
+
subparsers = parser.add_subparsers(dest="command", metavar="command")
|
|
49
|
+
|
|
50
|
+
# --- bg ---
|
|
51
|
+
p = subparsers.add_parser("bg", help="Remove background and center image")
|
|
52
|
+
p.add_argument("input", help="Source image path (PNG/JPG/WEBP)")
|
|
53
|
+
p.add_argument("output", help="Output PNG path")
|
|
54
|
+
p.add_argument("--size", type=int, default=1024, metavar="PX",
|
|
55
|
+
help="Canvas size in pixels (default: 1024)")
|
|
56
|
+
p.add_argument("--padding", type=int, default=40,
|
|
57
|
+
help="Edge padding in pixels (default: 40)")
|
|
58
|
+
|
|
59
|
+
# --- render ---
|
|
60
|
+
p = subparsers.add_parser("render", help="Render HTML template to JPG poster")
|
|
61
|
+
p.add_argument("html", help="HTML template path")
|
|
62
|
+
p.add_argument("output", help="Output JPG path")
|
|
63
|
+
p.add_argument("--browser", default=None,
|
|
64
|
+
help="Explicit browser executable path (auto-detected if omitted)")
|
|
65
|
+
|
|
66
|
+
# --- batch ---
|
|
67
|
+
p = subparsers.add_parser("batch", help="Parallel batch rendering: HTML folder -> JPG folder")
|
|
68
|
+
p.add_argument("html_dir", help="Directory containing HTML templates")
|
|
69
|
+
p.add_argument("output_dir", help="Directory for output JPG files")
|
|
70
|
+
p.add_argument("--workers", type=int, default=None,
|
|
71
|
+
help="Parallel thread count (default: auto, max 4)")
|
|
72
|
+
p.add_argument("--no-skip", action="store_true",
|
|
73
|
+
help="Re-render already existing JPG files")
|
|
74
|
+
p.add_argument("--browser", default=None,
|
|
75
|
+
help="Explicit browser executable path")
|
|
76
|
+
|
|
77
|
+
# --- patch ---
|
|
78
|
+
p = subparsers.add_parser("patch", help="Surgical texture patch by coordinates")
|
|
79
|
+
p.add_argument("image", help="Image path (modified in place)")
|
|
80
|
+
p.add_argument("x_start", type=int)
|
|
81
|
+
p.add_argument("x_end", type=int)
|
|
82
|
+
p.add_argument("y_start", type=int)
|
|
83
|
+
p.add_argument("y_end", type=int)
|
|
84
|
+
p.add_argument("--no-backup", action="store_true",
|
|
85
|
+
help="Skip .bak backup creation")
|
|
86
|
+
|
|
87
|
+
# --- autopatch ---
|
|
88
|
+
p = subparsers.add_parser("autopatch", help="BFS auto-patch by color (removes flags, labels)")
|
|
89
|
+
p.add_argument("image", help="Image path (modified in place)")
|
|
90
|
+
p.add_argument("r", type=int, help="Target color Red (0-255)")
|
|
91
|
+
p.add_argument("g", type=int, help="Target color Green (0-255)")
|
|
92
|
+
p.add_argument("b", type=int, help="Target color Blue (0-255)")
|
|
93
|
+
p.add_argument("--tolerance", type=int, default=30,
|
|
94
|
+
help="Color match tolerance (default: 30)")
|
|
95
|
+
p.add_argument("--no-backup", action="store_true",
|
|
96
|
+
help="Skip .bak backup creation")
|
|
97
|
+
|
|
98
|
+
# --- validate ---
|
|
99
|
+
p = subparsers.add_parser("validate", help="Validate local image assets in HTML template")
|
|
100
|
+
p.add_argument("html", help="HTML file path")
|
|
101
|
+
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
|
|
104
|
+
if args.command == "bg":
|
|
105
|
+
ImageProcessor.remove_background(
|
|
106
|
+
args.input, args.output,
|
|
107
|
+
target_size=(args.size, args.size),
|
|
108
|
+
padding=args.padding,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
elif args.command == "render":
|
|
112
|
+
renderer = PosterRenderer(browser_path=args.browser)
|
|
113
|
+
renderer.render(args.html, args.output)
|
|
114
|
+
|
|
115
|
+
elif args.command == "batch":
|
|
116
|
+
renderer = PosterRenderer(browser_path=args.browser)
|
|
117
|
+
html_files = sorted(
|
|
118
|
+
os.path.join(args.html_dir, f)
|
|
119
|
+
for f in os.listdir(args.html_dir) if f.endswith(".html")
|
|
120
|
+
)
|
|
121
|
+
jpg_files = [
|
|
122
|
+
os.path.join(args.output_dir, os.path.splitext(os.path.basename(f))[0] + ".jpg")
|
|
123
|
+
for f in html_files
|
|
124
|
+
]
|
|
125
|
+
renderer.render_batch(
|
|
126
|
+
html_files, jpg_files,
|
|
127
|
+
max_workers=args.workers,
|
|
128
|
+
skip_existing=not args.no_skip,
|
|
129
|
+
log_dir=args.output_dir,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
elif args.command == "patch":
|
|
133
|
+
ImageProcessor.patch_texture(
|
|
134
|
+
args.image, args.x_start, args.x_end, args.y_start, args.y_end,
|
|
135
|
+
backup=not args.no_backup,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
elif args.command == "autopatch":
|
|
139
|
+
ImageProcessor.auto_patch_by_color(
|
|
140
|
+
args.image, (args.r, args.g, args.b),
|
|
141
|
+
tolerance=args.tolerance,
|
|
142
|
+
backup=not args.no_backup,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
elif args.command == "validate":
|
|
146
|
+
ok = Validator.validate_html(args.html)
|
|
147
|
+
sys.exit(0 if ok else 1)
|
|
148
|
+
|
|
149
|
+
else:
|
|
150
|
+
print(_DESCRIPTION)
|
|
151
|
+
parser.print_help()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
main()
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ImageProcessor — background removal, texture patching, BFS color auto-patching.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import shutil
|
|
8
|
+
import logging
|
|
9
|
+
import subprocess
|
|
10
|
+
from io import BytesIO
|
|
11
|
+
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger("infographics_builder")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _create_backup(image_path: str) -> str:
|
|
18
|
+
"""Creates a .bak backup copy of the file before modification."""
|
|
19
|
+
bak_path = image_path + ".bak"
|
|
20
|
+
shutil.copy2(image_path, bak_path)
|
|
21
|
+
log.info(f"Backup created: {bak_path}")
|
|
22
|
+
return bak_path
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ImageProcessor:
|
|
26
|
+
"""
|
|
27
|
+
Image processing utilities: background removal, scaling,
|
|
28
|
+
centering and surgical texture patching.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
@staticmethod
|
|
32
|
+
def remove_background(
|
|
33
|
+
input_path: str,
|
|
34
|
+
output_path: str,
|
|
35
|
+
target_size: tuple = (1024, 1024),
|
|
36
|
+
padding: int = 40,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Remove image background using rembg, auto-crop to object bounds,
|
|
40
|
+
scale with preserved aspect ratio (Lanczos) and center on transparent canvas.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
input_path: Source image path (PNG/JPG/WEBP).
|
|
44
|
+
output_path: Output PNG path.
|
|
45
|
+
target_size: Canvas size in pixels, default (1024, 1024).
|
|
46
|
+
padding: Canvas edge padding in pixels, default 40.
|
|
47
|
+
"""
|
|
48
|
+
try:
|
|
49
|
+
from rembg import remove # type: ignore
|
|
50
|
+
except ImportError:
|
|
51
|
+
log.warning("rembg not found. Installing...")
|
|
52
|
+
subprocess.check_call([sys.executable, "-m", "pip", "install", "rembg"])
|
|
53
|
+
from rembg import remove # type: ignore
|
|
54
|
+
|
|
55
|
+
log.info(f"Removing background: {input_path}")
|
|
56
|
+
with open(input_path, "rb") as f:
|
|
57
|
+
img_data = f.read()
|
|
58
|
+
|
|
59
|
+
subject_data = remove(img_data)
|
|
60
|
+
img = Image.open(BytesIO(subject_data))
|
|
61
|
+
|
|
62
|
+
bbox = img.getbbox()
|
|
63
|
+
if not bbox:
|
|
64
|
+
raise ValueError(f"Image is empty after background removal: {input_path}")
|
|
65
|
+
|
|
66
|
+
cropped = img.crop(bbox)
|
|
67
|
+
width, height = cropped.size
|
|
68
|
+
max_dim = max(target_size) - (padding * 2)
|
|
69
|
+
|
|
70
|
+
if width >= height:
|
|
71
|
+
new_width = max_dim
|
|
72
|
+
new_height = int(height * (max_dim / width))
|
|
73
|
+
else:
|
|
74
|
+
new_height = max_dim
|
|
75
|
+
new_width = int(width * (max_dim / height))
|
|
76
|
+
|
|
77
|
+
resized = cropped.resize((new_width, new_height), Image.Resampling.LANCZOS)
|
|
78
|
+
final_img = Image.new("RGBA", target_size, (0, 0, 0, 0))
|
|
79
|
+
paste_x = (target_size[0] - new_width) // 2
|
|
80
|
+
paste_y = (target_size[1] - new_height) // 2
|
|
81
|
+
final_img.paste(resized, (paste_x, paste_y), resized)
|
|
82
|
+
|
|
83
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
|
84
|
+
final_img.save(output_path, "PNG")
|
|
85
|
+
log.info(f"Saved: {output_path}")
|
|
86
|
+
|
|
87
|
+
@staticmethod
|
|
88
|
+
def patch_texture(
|
|
89
|
+
image_path: str,
|
|
90
|
+
x_start: int,
|
|
91
|
+
x_end: int,
|
|
92
|
+
y_start: int,
|
|
93
|
+
y_end: int,
|
|
94
|
+
left_anchor_x: int = None,
|
|
95
|
+
right_anchor_x: int = None,
|
|
96
|
+
backup: bool = True,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""
|
|
99
|
+
Surgical rectangular zone patching with linear horizontal gradient
|
|
100
|
+
of color and alpha sampled from anchor pixels outside the zone.
|
|
101
|
+
|
|
102
|
+
Args:
|
|
103
|
+
image_path: Image path (OVERWRITTEN in place).
|
|
104
|
+
x_start/x_end: Horizontal boundaries of the patch zone.
|
|
105
|
+
y_start/y_end: Vertical boundaries of the patch zone.
|
|
106
|
+
left_anchor_x: X of left color anchor (default: x_start - 3).
|
|
107
|
+
right_anchor_x: X of right color anchor (default: x_end + 3).
|
|
108
|
+
backup: If True, creates .bak backup before modification.
|
|
109
|
+
"""
|
|
110
|
+
if backup:
|
|
111
|
+
_create_backup(image_path)
|
|
112
|
+
|
|
113
|
+
img = Image.open(image_path)
|
|
114
|
+
width, _ = img.size
|
|
115
|
+
pixels = img.load()
|
|
116
|
+
|
|
117
|
+
if left_anchor_x is None:
|
|
118
|
+
left_anchor_x = max(0, x_start - 3)
|
|
119
|
+
if right_anchor_x is None:
|
|
120
|
+
right_anchor_x = min(width - 1, x_end + 3)
|
|
121
|
+
|
|
122
|
+
total_width = right_anchor_x - left_anchor_x
|
|
123
|
+
if total_width == 0:
|
|
124
|
+
log.warning("Anchors coincide, patching impossible.")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
for y in range(y_start, y_end + 1):
|
|
128
|
+
left_color = pixels[left_anchor_x, y]
|
|
129
|
+
right_color = pixels[right_anchor_x, y]
|
|
130
|
+
for x in range(x_start, x_end + 1):
|
|
131
|
+
t = (x - left_anchor_x) / total_width
|
|
132
|
+
r = int(left_color[0] * (1 - t) + right_color[0] * t)
|
|
133
|
+
g = int(left_color[1] * (1 - t) + right_color[1] * t)
|
|
134
|
+
b = int(left_color[2] * (1 - t) + right_color[2] * t)
|
|
135
|
+
if len(left_color) > 3:
|
|
136
|
+
a = int(left_color[3] * (1 - t) + right_color[3] * t)
|
|
137
|
+
pixels[x, y] = (r, g, b, a)
|
|
138
|
+
else:
|
|
139
|
+
pixels[x, y] = (r, g, b)
|
|
140
|
+
|
|
141
|
+
img.save(image_path)
|
|
142
|
+
log.info(f"Patched zone [{x_start}:{x_end}, {y_start}:{y_end}]")
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def auto_patch_by_color(
|
|
146
|
+
image_path: str,
|
|
147
|
+
target_color_rgb: tuple,
|
|
148
|
+
tolerance: int = 30,
|
|
149
|
+
min_area_size: int = 4,
|
|
150
|
+
padding: int = 2,
|
|
151
|
+
backup: bool = True,
|
|
152
|
+
) -> None:
|
|
153
|
+
"""
|
|
154
|
+
BFS scan of connected color regions matching target_color_rgb and
|
|
155
|
+
automatic texture patching of each found region.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
image_path: Image path (OVERWRITTEN in place).
|
|
159
|
+
target_color_rgb: Target color as (R, G, B).
|
|
160
|
+
tolerance: Max Euclidean RGB distance from target color.
|
|
161
|
+
min_area_size: Minimum connected region size (pixels) to patch.
|
|
162
|
+
padding: Extra pixels to extend patch zone around region.
|
|
163
|
+
backup: If True, creates .bak backup before modification.
|
|
164
|
+
"""
|
|
165
|
+
if backup:
|
|
166
|
+
_create_backup(image_path)
|
|
167
|
+
|
|
168
|
+
log.info(f"Auto-patch by color {target_color_rgb} (tolerance={tolerance}): {image_path}")
|
|
169
|
+
img = Image.open(image_path)
|
|
170
|
+
width, height = img.size
|
|
171
|
+
pixels = img.load()
|
|
172
|
+
|
|
173
|
+
visited: set = set()
|
|
174
|
+
regions: list = []
|
|
175
|
+
|
|
176
|
+
def is_target(pixel):
|
|
177
|
+
r, g, b = pixel[:3]
|
|
178
|
+
tr, tg, tb = target_color_rgb
|
|
179
|
+
return ((r - tr) ** 2 + (g - tg) ** 2 + (b - tb) ** 2) ** 0.5 <= tolerance
|
|
180
|
+
|
|
181
|
+
for x in range(width):
|
|
182
|
+
for y in range(height):
|
|
183
|
+
if (x, y) in visited:
|
|
184
|
+
continue
|
|
185
|
+
px = pixels[x, y]
|
|
186
|
+
if len(px) > 3 and px[3] < 50:
|
|
187
|
+
continue
|
|
188
|
+
if is_target(px):
|
|
189
|
+
queue = [(x, y)]
|
|
190
|
+
visited.add((x, y))
|
|
191
|
+
region = []
|
|
192
|
+
while queue:
|
|
193
|
+
cx, cy = queue.pop(0)
|
|
194
|
+
region.append((cx, cy))
|
|
195
|
+
for nx, ny in [(cx - 1, cy), (cx + 1, cy), (cx, cy - 1), (cx, cy + 1)]:
|
|
196
|
+
if 0 <= nx < width and 0 <= ny < height and (nx, ny) not in visited:
|
|
197
|
+
npx = pixels[nx, ny]
|
|
198
|
+
if len(npx) > 3 and npx[3] < 50:
|
|
199
|
+
continue
|
|
200
|
+
if is_target(npx):
|
|
201
|
+
visited.add((nx, ny))
|
|
202
|
+
queue.append((nx, ny))
|
|
203
|
+
if len(region) >= min_area_size:
|
|
204
|
+
regions.append(region)
|
|
205
|
+
|
|
206
|
+
log.info(f"Regions found: {len(regions)}")
|
|
207
|
+
if not regions:
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
for idx, region in enumerate(regions):
|
|
211
|
+
xs = [p[0] for p in region]
|
|
212
|
+
ys = [p[1] for p in region]
|
|
213
|
+
min_x, max_x = min(xs), max(xs)
|
|
214
|
+
min_y, max_y = min(ys), max(ys)
|
|
215
|
+
|
|
216
|
+
x_s = max(0, min_x - padding)
|
|
217
|
+
x_e = min(width - 1, max_x + padding)
|
|
218
|
+
y_s = max(0, min_y - padding)
|
|
219
|
+
y_e = min(height - 1, max_y + padding)
|
|
220
|
+
left_anc = max(0, x_s - 3)
|
|
221
|
+
right_anc = min(width - 1, x_e + 3)
|
|
222
|
+
|
|
223
|
+
log.info(f" Region #{idx + 1}: X({min_x}..{max_x}), Y({min_y}..{max_y})")
|
|
224
|
+
ImageProcessor.patch_texture(image_path, x_s, x_e, y_s, y_e, left_anc, right_anc, backup=False)
|
|
225
|
+
|
|
226
|
+
log.info("Auto-patch complete.")
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PosterRenderer — headless HTML-to-JPG rendering (single & parallel batch).
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import logging
|
|
8
|
+
import subprocess
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
11
|
+
|
|
12
|
+
from PIL import Image
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger("infographics_builder")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class PosterRenderer:
|
|
18
|
+
"""
|
|
19
|
+
Renders HTML templates into print-quality JPG posters (2400x3600px)
|
|
20
|
+
via headless Brave or Chrome browser.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(self, browser_path: str = None):
|
|
24
|
+
"""
|
|
25
|
+
Args:
|
|
26
|
+
browser_path: Explicit path to browser executable.
|
|
27
|
+
If not provided, auto-detected from standard locations.
|
|
28
|
+
"""
|
|
29
|
+
self.browser_path = browser_path or self._find_browser()
|
|
30
|
+
if not self.browser_path:
|
|
31
|
+
raise FileNotFoundError(
|
|
32
|
+
"Brave or Chrome not found. Specify manually: PosterRenderer(browser_path=...)"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
def _find_browser(self) -> str | None:
|
|
36
|
+
"""Auto-detect Brave or Chrome on Windows and macOS."""
|
|
37
|
+
candidates = [
|
|
38
|
+
# Windows — Brave
|
|
39
|
+
r"C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe",
|
|
40
|
+
r"C:\Program Files (x86)\BraveSoftware\Brave-Browser\Application\brave.exe",
|
|
41
|
+
os.path.expandvars(r"%LOCALAPPDATA%\BraveSoftware\Brave-Browser\Application\brave.exe"),
|
|
42
|
+
# Windows — Chrome
|
|
43
|
+
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
|
|
44
|
+
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
|
|
45
|
+
os.path.expandvars(r"%LOCALAPPDATA%\Google\Chrome\Application\chrome.exe"),
|
|
46
|
+
# macOS
|
|
47
|
+
"/Applications/Brave Browser.app/Contents/MacOS/Brave Browser",
|
|
48
|
+
"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
|
|
49
|
+
]
|
|
50
|
+
for p in candidates:
|
|
51
|
+
if os.path.exists(p):
|
|
52
|
+
return p
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
def render(self, html_path: str, output_jpg_path: str) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Render a single HTML template to a 2400x3600px JPG poster.
|
|
58
|
+
|
|
59
|
+
Uses headless browser with:
|
|
60
|
+
--window-size=1200,1800 --force-device-scale-factor=2
|
|
61
|
+
producing a 2x DPR screenshot = 2400x3600px.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
html_path: Path to the HTML template file.
|
|
65
|
+
output_jpg_path: Path for the output JPG.
|
|
66
|
+
"""
|
|
67
|
+
if not os.path.exists(html_path):
|
|
68
|
+
raise FileNotFoundError(f"HTML template not found: {html_path}")
|
|
69
|
+
|
|
70
|
+
temp_png = output_jpg_path.replace(".jpg", "_temp.png")
|
|
71
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_jpg_path)), exist_ok=True)
|
|
72
|
+
|
|
73
|
+
file_url = f"file:///{os.path.abspath(html_path).replace(os.sep, '/')}"
|
|
74
|
+
cmd = [
|
|
75
|
+
self.browser_path,
|
|
76
|
+
"--headless",
|
|
77
|
+
"--disable-gpu",
|
|
78
|
+
f"--screenshot={temp_png}",
|
|
79
|
+
"--window-size=1200,1800",
|
|
80
|
+
"--force-device-scale-factor=2",
|
|
81
|
+
"--hide-scrollbars",
|
|
82
|
+
"--run-all-compositor-stages-before-draw",
|
|
83
|
+
file_url,
|
|
84
|
+
]
|
|
85
|
+
subprocess.run(cmd, check=True, capture_output=True)
|
|
86
|
+
|
|
87
|
+
if not os.path.exists(temp_png):
|
|
88
|
+
raise RuntimeError(f"Browser did not produce screenshot: {temp_png}")
|
|
89
|
+
|
|
90
|
+
img = Image.open(temp_png)
|
|
91
|
+
img.convert("RGB").save(output_jpg_path, "JPEG", quality=95)
|
|
92
|
+
os.remove(temp_png)
|
|
93
|
+
log.info(f"Rendered: {os.path.basename(output_jpg_path)}")
|
|
94
|
+
|
|
95
|
+
def render_batch(
|
|
96
|
+
self,
|
|
97
|
+
html_paths: list,
|
|
98
|
+
output_jpg_paths: list,
|
|
99
|
+
max_workers: int = None,
|
|
100
|
+
skip_existing: bool = True,
|
|
101
|
+
log_dir: str = None,
|
|
102
|
+
) -> dict:
|
|
103
|
+
"""
|
|
104
|
+
Parallel batch rendering of multiple posters with progress bar (tqdm)
|
|
105
|
+
and automatic build log file.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
html_paths: List of HTML template paths.
|
|
109
|
+
output_jpg_paths: List of output JPG paths (1:1 with html_paths).
|
|
110
|
+
max_workers: Thread pool size. Default: min(4, cpu_count // 2).
|
|
111
|
+
skip_existing: Skip already-rendered JPG files. Default: True.
|
|
112
|
+
log_dir: Directory for build_YYYY-MM-DD.log file.
|
|
113
|
+
Default: directory of first output JPG.
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
dict with keys "ok" (list) and "failed" (list).
|
|
117
|
+
"""
|
|
118
|
+
if len(html_paths) != len(output_jpg_paths):
|
|
119
|
+
raise ValueError("html_paths and output_jpg_paths must have the same length.")
|
|
120
|
+
|
|
121
|
+
if not max_workers:
|
|
122
|
+
max_workers = max(1, min(4, (os.cpu_count() or 2) // 2))
|
|
123
|
+
|
|
124
|
+
if log_dir is None:
|
|
125
|
+
log_dir = os.path.dirname(os.path.abspath(output_jpg_paths[0]))
|
|
126
|
+
os.makedirs(log_dir, exist_ok=True)
|
|
127
|
+
|
|
128
|
+
log_path = os.path.join(log_dir, f"build_{datetime.now().strftime('%Y-%m-%d')}.log")
|
|
129
|
+
file_handler = logging.FileHandler(log_path, encoding="utf-8")
|
|
130
|
+
file_handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(message)s"))
|
|
131
|
+
log.addHandler(file_handler)
|
|
132
|
+
log.info(f"=== Batch render | workers={max_workers} | total={len(html_paths)} ===")
|
|
133
|
+
|
|
134
|
+
tasks = []
|
|
135
|
+
skipped = 0
|
|
136
|
+
for html, jpg in zip(html_paths, output_jpg_paths):
|
|
137
|
+
if skip_existing and os.path.exists(jpg):
|
|
138
|
+
log.info(f"[SKIP] {os.path.basename(jpg)}")
|
|
139
|
+
skipped += 1
|
|
140
|
+
else:
|
|
141
|
+
tasks.append((html, jpg))
|
|
142
|
+
|
|
143
|
+
if skipped:
|
|
144
|
+
log.info(f"Skipped (already exist): {skipped}")
|
|
145
|
+
|
|
146
|
+
results = {"ok": [], "failed": []}
|
|
147
|
+
|
|
148
|
+
if not tasks:
|
|
149
|
+
log.info("No tasks to render.")
|
|
150
|
+
log.removeHandler(file_handler)
|
|
151
|
+
return results
|
|
152
|
+
|
|
153
|
+
try:
|
|
154
|
+
from tqdm import tqdm # type: ignore
|
|
155
|
+
progress = tqdm(total=len(tasks), unit="poster", desc="Rendering")
|
|
156
|
+
use_tqdm = True
|
|
157
|
+
except ImportError:
|
|
158
|
+
log.warning("tqdm not installed (pip install tqdm). Text progress only.")
|
|
159
|
+
use_tqdm = False
|
|
160
|
+
|
|
161
|
+
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
|
162
|
+
futures = {
|
|
163
|
+
executor.submit(self.render, html, jpg): (html, jpg)
|
|
164
|
+
for html, jpg in tasks
|
|
165
|
+
}
|
|
166
|
+
for future in as_completed(futures):
|
|
167
|
+
html, jpg = futures[future]
|
|
168
|
+
try:
|
|
169
|
+
future.result()
|
|
170
|
+
results["ok"].append(jpg)
|
|
171
|
+
log.info(f"[OK] {os.path.basename(jpg)}")
|
|
172
|
+
except Exception as exc:
|
|
173
|
+
results["failed"].append(jpg)
|
|
174
|
+
log.error(f"[FAIL] {os.path.basename(html)}: {exc}")
|
|
175
|
+
finally:
|
|
176
|
+
if use_tqdm:
|
|
177
|
+
progress.update(1)
|
|
178
|
+
|
|
179
|
+
if use_tqdm:
|
|
180
|
+
progress.close()
|
|
181
|
+
|
|
182
|
+
log.info(f"=== Done | ok={len(results['ok'])} | failed={len(results['failed'])} ===")
|
|
183
|
+
log.info(f"Log saved: {log_path}")
|
|
184
|
+
log.removeHandler(file_handler)
|
|
185
|
+
return results
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Validator — pre-render HTML asset validation.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import logging
|
|
8
|
+
|
|
9
|
+
log = logging.getLogger("infographics_builder")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Validator:
|
|
13
|
+
"""Pre-render validation of HTML template local assets."""
|
|
14
|
+
|
|
15
|
+
@staticmethod
|
|
16
|
+
def validate_html(html_path: str) -> bool:
|
|
17
|
+
"""
|
|
18
|
+
Check that all local images referenced in the HTML file exist on disk.
|
|
19
|
+
|
|
20
|
+
Args:
|
|
21
|
+
html_path: Path to the HTML file to validate.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
True if all local assets exist, False otherwise.
|
|
25
|
+
"""
|
|
26
|
+
if not os.path.exists(html_path):
|
|
27
|
+
log.error(f"File does not exist: {html_path}")
|
|
28
|
+
return False
|
|
29
|
+
|
|
30
|
+
log.info(f"Validating: {html_path}")
|
|
31
|
+
html_dir = os.path.dirname(os.path.abspath(html_path))
|
|
32
|
+
|
|
33
|
+
with open(html_path, "r", encoding="utf-8") as f:
|
|
34
|
+
content = f.read()
|
|
35
|
+
|
|
36
|
+
sources = re.findall(r'src=["\']([^"\']+)["\']', content)
|
|
37
|
+
all_ok = True
|
|
38
|
+
|
|
39
|
+
for src in sources:
|
|
40
|
+
if src.startswith(("http://", "https://", "data:")):
|
|
41
|
+
continue
|
|
42
|
+
clean_src = src.replace("../", "").replace("file:///", "")
|
|
43
|
+
paths_to_check = [
|
|
44
|
+
os.path.join(html_dir, src),
|
|
45
|
+
os.path.join(html_dir, clean_src),
|
|
46
|
+
os.path.join(os.path.dirname(html_dir), clean_src),
|
|
47
|
+
]
|
|
48
|
+
found = any(os.path.exists(p) for p in paths_to_check)
|
|
49
|
+
if found:
|
|
50
|
+
log.info(f" [OK] {src}")
|
|
51
|
+
else:
|
|
52
|
+
log.error(f" [MISSING] {src}")
|
|
53
|
+
all_ok = False
|
|
54
|
+
|
|
55
|
+
if all_ok:
|
|
56
|
+
log.info("[SUCCESS] All local assets found.")
|
|
57
|
+
else:
|
|
58
|
+
log.warning("[INCOMPLETE] Some assets are missing.")
|
|
59
|
+
return all_ok
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: infographics-builder
|
|
3
|
+
Version: 2.0.0
|
|
4
|
+
Summary: CLI library for automated infographic poster generation: background removal, parallel headless rendering, color-based auto-patching and HTML asset validation.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/yourusername/infographics-builder
|
|
7
|
+
Project-URL: Issues, https://github.com/yourusername/infographics-builder/issues
|
|
8
|
+
Keywords: infographics,poster,rendering,rembg,pillow,background-removal,headless-chrome
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Multimedia :: Graphics
|
|
16
|
+
Classifier: Topic :: Utilities
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: MacOS
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Requires-Dist: Pillow>=10.0.0
|
|
23
|
+
Requires-Dist: rembg>=2.0.0
|
|
24
|
+
Provides-Extra: progress
|
|
25
|
+
Requires-Dist: tqdm>=4.60.0; extra == "progress"
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# infographics-builder
|
|
29
|
+
|
|
30
|
+
> CLI Python library for automated generation of high-quality infographic posters.
|
|
31
|
+
> Background removal • Parallel headless rendering • BFS color auto-patching • Asset validation
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Features
|
|
36
|
+
|
|
37
|
+
| Command | Description |
|
|
38
|
+
|---------|-------------|
|
|
39
|
+
| `bg` | Remove background via `rembg` and center object on transparent canvas |
|
|
40
|
+
| `render` | Render HTML template to print-quality JPG (2400×3600px) via headless Brave/Chrome |
|
|
41
|
+
| `batch` | Parallel batch rendering of entire folder with progress bar and build log |
|
|
42
|
+
| `patch` | Surgical rectangular texture patching via linear color interpolation |
|
|
43
|
+
| `autopatch` | BFS-based automatic color region detection and patching (removes flags, labels) |
|
|
44
|
+
| `validate` | Pre-render validation of all local image assets in HTML template |
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Installation
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install infographics-builder
|
|
52
|
+
|
|
53
|
+
# Optional: progress bar support
|
|
54
|
+
pip install infographics-builder[progress]
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**Requirements:**
|
|
58
|
+
- Python 3.10+
|
|
59
|
+
- Brave Browser or Google Chrome (for `render` / `batch` commands)
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
### Remove background from image
|
|
66
|
+
```bash
|
|
67
|
+
infographics-builder bg input_photo.jpg output.png
|
|
68
|
+
infographics-builder bg input_photo.jpg output.png --size 1024 --padding 40
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Render a single poster
|
|
72
|
+
```bash
|
|
73
|
+
infographics-builder render card.html poster.jpg
|
|
74
|
+
infographics-builder render card.html poster.jpg --browser "C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Batch render entire folder (with skip-existing and auto log)
|
|
78
|
+
```bash
|
|
79
|
+
infographics-builder batch ./html_pages/ ./renders/
|
|
80
|
+
infographics-builder batch ./html_pages/ ./renders/ --workers 4 --no-skip
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Auto-patch unwanted color regions (flags, watermarks)
|
|
84
|
+
```bash
|
|
85
|
+
# Remove red (255,0,0) with tolerance 40
|
|
86
|
+
infographics-builder autopatch model.png 255 0 0 --tolerance 40
|
|
87
|
+
|
|
88
|
+
# Remove blue flag without creating a backup
|
|
89
|
+
infographics-builder autopatch model.png 0 0 200 --tolerance 30 --no-backup
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Manual texture patch by coordinates
|
|
93
|
+
```bash
|
|
94
|
+
infographics-builder patch model.png 580 620 340 380
|
|
95
|
+
infographics-builder patch model.png 580 620 340 380 --no-backup
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Validate HTML template assets
|
|
99
|
+
```bash
|
|
100
|
+
infographics-builder validate card.html
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Python API
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from infographics_builder import ImageProcessor, PosterRenderer, Validator
|
|
109
|
+
|
|
110
|
+
# Remove background
|
|
111
|
+
ImageProcessor.remove_background("photo.jpg", "output.png")
|
|
112
|
+
|
|
113
|
+
# Render poster
|
|
114
|
+
renderer = PosterRenderer() # auto-detects Brave/Chrome
|
|
115
|
+
renderer.render("card.html", "poster.jpg")
|
|
116
|
+
|
|
117
|
+
# Batch render with progress bar
|
|
118
|
+
renderer.render_batch(
|
|
119
|
+
html_paths=["card1.html", "card2.html"],
|
|
120
|
+
output_jpg_paths=["poster1.jpg", "poster2.jpg"],
|
|
121
|
+
skip_existing=True, # skip already rendered
|
|
122
|
+
log_dir="./logs/" # write build_YYYY-MM-DD.log
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
# Auto-patch by color (BFS)
|
|
126
|
+
ImageProcessor.auto_patch_by_color("model.png", target_color_rgb=(255, 0, 0), tolerance=30)
|
|
127
|
+
|
|
128
|
+
# Validate assets
|
|
129
|
+
ok = Validator.validate_html("card.html")
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
---
|
|
133
|
+
|
|
134
|
+
## Rendering Architecture
|
|
135
|
+
|
|
136
|
+
The `render` command uses a Chromium-based headless browser with these flags:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
--window-size=1200,1800 --force-device-scale-factor=2
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
This produces a **2400×3600px** screenshot (device-pixel-ratio 2x), which is then converted to JPG at 95% quality via Pillow.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Safety Features
|
|
147
|
+
|
|
148
|
+
- **Auto-backup (`.bak`)**: `patch` and `autopatch` create a `.bak` copy before modifying any file. Disable with `--no-backup`.
|
|
149
|
+
- **Skip existing**: `batch` skips already-rendered JPGs by default (`--no-skip` to override).
|
|
150
|
+
- **Build log**: Every `batch` run writes `build_YYYY-MM-DD.log` in the output directory.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
infographics_builder/__init__.py
|
|
5
|
+
infographics_builder/cli.py
|
|
6
|
+
infographics_builder/processor.py
|
|
7
|
+
infographics_builder/renderer.py
|
|
8
|
+
infographics_builder/validator.py
|
|
9
|
+
infographics_builder.egg-info/PKG-INFO
|
|
10
|
+
infographics_builder.egg-info/SOURCES.txt
|
|
11
|
+
infographics_builder.egg-info/dependency_links.txt
|
|
12
|
+
infographics_builder.egg-info/entry_points.txt
|
|
13
|
+
infographics_builder.egg-info/requires.txt
|
|
14
|
+
infographics_builder.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
infographics_builder
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "infographics-builder"
|
|
7
|
+
version = "2.0.0"
|
|
8
|
+
description = "CLI library for automated infographic poster generation: background removal, parallel headless rendering, color-based auto-patching and HTML asset validation."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
keywords = ["infographics", "poster", "rendering", "rembg", "pillow", "background-removal", "headless-chrome"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Development Status :: 4 - Beta",
|
|
15
|
+
"Intended Audience :: Developers",
|
|
16
|
+
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Multimedia :: Graphics",
|
|
22
|
+
"Topic :: Utilities",
|
|
23
|
+
"Operating System :: Microsoft :: Windows",
|
|
24
|
+
"Operating System :: MacOS",
|
|
25
|
+
]
|
|
26
|
+
dependencies = [
|
|
27
|
+
"Pillow>=10.0.0",
|
|
28
|
+
"rembg>=2.0.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.optional-dependencies]
|
|
32
|
+
progress = ["tqdm>=4.60.0"]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
infographics-builder = "infographics_builder.cli:main"
|
|
36
|
+
|
|
37
|
+
[project.urls]
|
|
38
|
+
Homepage = "https://github.com/yourusername/infographics-builder"
|
|
39
|
+
Issues = "https://github.com/yourusername/infographics-builder/issues"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["."]
|
|
43
|
+
include = ["infographics_builder*"]
|