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.
@@ -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,2 @@
1
+ [console_scripts]
2
+ infographics-builder = infographics_builder.cli:main
@@ -0,0 +1,5 @@
1
+ Pillow>=10.0.0
2
+ rembg>=2.0.0
3
+
4
+ [progress]
5
+ tqdm>=4.60.0
@@ -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*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+