postcanvas 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.
- postcanvas-0.1.0/PKG-INFO +165 -0
- postcanvas-0.1.0/README.md +148 -0
- postcanvas-0.1.0/postcanvas/__init__.py +31 -0
- postcanvas-0.1.0/postcanvas/models/__init__.py +24 -0
- postcanvas-0.1.0/postcanvas/models/background.py +23 -0
- postcanvas-0.1.0/postcanvas/models/canvas.py +49 -0
- postcanvas-0.1.0/postcanvas/models/elements.py +74 -0
- postcanvas-0.1.0/postcanvas/models/enums.py +96 -0
- postcanvas-0.1.0/postcanvas/models/meta.py +12 -0
- postcanvas-0.1.0/postcanvas/models/post.py +66 -0
- postcanvas-0.1.0/postcanvas/models/primitives.py +57 -0
- postcanvas-0.1.0/postcanvas/models/text.py +54 -0
- postcanvas-0.1.0/postcanvas/models/watermark.py +15 -0
- postcanvas-0.1.0/postcanvas/presets/__init__.py +22 -0
- postcanvas-0.1.0/postcanvas/presets/platforms.py +64 -0
- postcanvas-0.1.0/postcanvas/renderer/__init__.py +2 -0
- postcanvas-0.1.0/postcanvas/renderer/background.py +42 -0
- postcanvas-0.1.0/postcanvas/renderer/canvas.py +163 -0
- postcanvas-0.1.0/postcanvas/renderer/filters.py +49 -0
- postcanvas-0.1.0/postcanvas/renderer/gradient.py +43 -0
- postcanvas-0.1.0/postcanvas/renderer/images.py +120 -0
- postcanvas-0.1.0/postcanvas/renderer/loader.py +37 -0
- postcanvas-0.1.0/postcanvas/renderer/shapes.py +123 -0
- postcanvas-0.1.0/postcanvas/renderer/text.py +226 -0
- postcanvas-0.1.0/postcanvas/renderer/utils.py +62 -0
- postcanvas-0.1.0/postcanvas/utils/__init__.py +1 -0
- postcanvas-0.1.0/postcanvas.egg-info/PKG-INFO +165 -0
- postcanvas-0.1.0/postcanvas.egg-info/SOURCES.txt +31 -0
- postcanvas-0.1.0/postcanvas.egg-info/dependency_links.txt +1 -0
- postcanvas-0.1.0/postcanvas.egg-info/requires.txt +9 -0
- postcanvas-0.1.0/postcanvas.egg-info/top_level.txt +1 -0
- postcanvas-0.1.0/pyproject.toml +25 -0
- postcanvas-0.1.0/setup.cfg +4 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: postcanvas
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Generate pixel-perfect social media images from Python Pydantic models
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: social media,instagram,image generation,pillow,pydantic
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
Requires-Dist: Pillow>=10.0
|
|
10
|
+
Requires-Dist: pydantic>=2.0
|
|
11
|
+
Requires-Dist: requests>=2.28
|
|
12
|
+
Requires-Dist: numpy>=1.24
|
|
13
|
+
Provides-Extra: dev
|
|
14
|
+
Requires-Dist: pytest; extra == "dev"
|
|
15
|
+
Requires-Dist: black; extra == "dev"
|
|
16
|
+
Requires-Dist: ruff; extra == "dev"
|
|
17
|
+
|
|
18
|
+
# postcanvas 🎨
|
|
19
|
+
|
|
20
|
+
> Generate pixel-perfect social-media images from Python — just describe what you want.
|
|
21
|
+
|
|
22
|
+
## Install
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install postcanvas
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Quick start
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from postcanvas import generate
|
|
32
|
+
from postcanvas.presets import instagram_post
|
|
33
|
+
from postcanvas.models import BackgroundConfig, TextConfig, ShadowConfig
|
|
34
|
+
|
|
35
|
+
post = instagram_post(
|
|
36
|
+
background=BackgroundConfig(color="#1a1a2e"),
|
|
37
|
+
texts=[
|
|
38
|
+
TextConfig(
|
|
39
|
+
content="Hello World!",
|
|
40
|
+
y="50%",
|
|
41
|
+
font_size=96,
|
|
42
|
+
color="#e94560",
|
|
43
|
+
shadow=ShadowConfig(blur_radius=12),
|
|
44
|
+
)
|
|
45
|
+
],
|
|
46
|
+
output_dir="./output",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
generate(post) # → ./output/post.png
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Platforms & formats
|
|
53
|
+
|
|
54
|
+
| Helper | Size |
|
|
55
|
+
|---|---|
|
|
56
|
+
| `instagram_post()` | 1080 × 1080 |
|
|
57
|
+
| `instagram_portrait()` | 1080 × 1350 |
|
|
58
|
+
| `instagram_story()` | 1080 × 1920 |
|
|
59
|
+
| `x_post()` | 1600 × 900 |
|
|
60
|
+
| `reddit_post()` | 1920 × 1080 |
|
|
61
|
+
| `blog_og()` | 1200 × 628 |
|
|
62
|
+
| `linkedin_post()` | 1080 × 1080 |
|
|
63
|
+
| `youtube_thumbnail()` | 1280 × 720 |
|
|
64
|
+
| `facebook_post()` | 1080 × 1080 |
|
|
65
|
+
| `tiktok_story()` | 1080 × 1920 |
|
|
66
|
+
|
|
67
|
+
Use `preset(Platform.CUSTOM, PostFormat.CUSTOM, width=800, height=600)` for custom sizes.
|
|
68
|
+
|
|
69
|
+
## Carousel / multi-image
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
from postcanvas.models import CanvasConfig, BackgroundConfig
|
|
73
|
+
|
|
74
|
+
post = instagram_post(
|
|
75
|
+
canvases=[
|
|
76
|
+
CanvasConfig(background=BackgroundConfig(color="#e94560"),
|
|
77
|
+
texts=[TextConfig(content="Slide 1", ...)]),
|
|
78
|
+
CanvasConfig(background=BackgroundConfig(color="#0f3460"),
|
|
79
|
+
texts=[TextConfig(content="Slide 2", ...)]),
|
|
80
|
+
]
|
|
81
|
+
)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## Key model reference
|
|
85
|
+
|
|
86
|
+
### `PostConfig` (root)
|
|
87
|
+
| Field | Type | Description |
|
|
88
|
+
|---|---|---|
|
|
89
|
+
| `platform` | `Platform` | Target platform |
|
|
90
|
+
| `width` / `height` | `int` | Canvas size in px |
|
|
91
|
+
| `background` | `BackgroundConfig` | Global background |
|
|
92
|
+
| `padding` | `PaddingConfig` | Safe-area insets |
|
|
93
|
+
| `texts` | `List[TextConfig]` | Global text elements |
|
|
94
|
+
| `images` | `List[ImageElementConfig]` | Global image elements |
|
|
95
|
+
| `shapes` | `List[ShapeConfig]` | Global shapes |
|
|
96
|
+
| `canvases` | `List[CanvasConfig]` | Slides (carousel) |
|
|
97
|
+
| `watermark` | `WatermarkConfig` | Applied to every slide |
|
|
98
|
+
| `output_dir` | `str` | Where to save files |
|
|
99
|
+
| `output_format` | `OutputFormat` | `png` / `jpeg` / `webp` |
|
|
100
|
+
|
|
101
|
+
### Positioning
|
|
102
|
+
Every `x`, `y`, `width`, `height` accepts:
|
|
103
|
+
- **Absolute pixels**: `540`, `200`
|
|
104
|
+
- **Relative string**: `"50%"`, `"80%"`
|
|
105
|
+
|
|
106
|
+
### Anchors
|
|
107
|
+
`anchor` can be: `topleft`, `topcenter`, `topright`, `left`, `center`,
|
|
108
|
+
`right`, `bottomleft`, `bottomcenter`, `bottomright`
|
|
109
|
+
|
|
110
|
+
### z_index
|
|
111
|
+
Elements are composited in ascending `z_index` order across all types
|
|
112
|
+
(shapes, images, texts). Default values: shapes=1, images=5, texts=10.
|
|
113
|
+
|
|
114
|
+
### Text inside images and shapes
|
|
115
|
+
Both `ImageElementConfig` and `ShapeConfig` now support a `texts` list:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from postcanvas.models import ImageElementConfig, ShapeConfig, ShapeType, TextConfig
|
|
119
|
+
|
|
120
|
+
ShapeConfig(
|
|
121
|
+
type=ShapeType.ROUNDED_RECTANGLE,
|
|
122
|
+
x="50%", y="35%", width="70%", height="30%", anchor="center",
|
|
123
|
+
fill_color="#1f3b4d",
|
|
124
|
+
texts=[
|
|
125
|
+
TextConfig(content="Inside Shape", x="50%", y="50%", anchor="center")
|
|
126
|
+
],
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
ImageElementConfig(
|
|
130
|
+
src="assets/photo.jpg",
|
|
131
|
+
x="50%", y="70%", width="60%", height="35%", anchor="center",
|
|
132
|
+
texts=[
|
|
133
|
+
TextConfig(content="Inside Image", x="50%", y="88%", anchor="bottomcenter")
|
|
134
|
+
],
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
Nested text coordinates are resolved relative to the element's own box, not the full canvas.
|
|
139
|
+
|
|
140
|
+
### Font inheritance (Post > Canvas > Text override)
|
|
141
|
+
You can define default text font at post level, override it per canvas, and still override per text:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from postcanvas.presets import instagram_post
|
|
145
|
+
from postcanvas.models import CanvasConfig, TextConfig
|
|
146
|
+
|
|
147
|
+
post = instagram_post(
|
|
148
|
+
text_font_path="Roboto/static/Roboto-Regular.ttf", # default for whole post
|
|
149
|
+
texts=[
|
|
150
|
+
TextConfig(content="Uses post default", x="50%", y="15%"),
|
|
151
|
+
TextConfig(content="Custom text font", x="50%", y="25%", font_path="Roboto/static/Roboto-Bold.ttf"),
|
|
152
|
+
],
|
|
153
|
+
canvases=[
|
|
154
|
+
CanvasConfig(
|
|
155
|
+
text_font_path="Roboto/static/Roboto-Italic.ttf", # overrides post default on this slide
|
|
156
|
+
texts=[
|
|
157
|
+
TextConfig(content="Uses canvas override", x="50%", y="50%"),
|
|
158
|
+
TextConfig(content="Text-level still wins", x="50%", y="60%", font_path="Roboto/static/Roboto-Medium.ttf"),
|
|
159
|
+
],
|
|
160
|
+
)
|
|
161
|
+
],
|
|
162
|
+
)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Precedence: `TextConfig` > `CanvasConfig` > `PostConfig` > internal Arial fallback.
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# postcanvas 🎨
|
|
2
|
+
|
|
3
|
+
> Generate pixel-perfect social-media images from Python — just describe what you want.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install postcanvas
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick start
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from postcanvas import generate
|
|
15
|
+
from postcanvas.presets import instagram_post
|
|
16
|
+
from postcanvas.models import BackgroundConfig, TextConfig, ShadowConfig
|
|
17
|
+
|
|
18
|
+
post = instagram_post(
|
|
19
|
+
background=BackgroundConfig(color="#1a1a2e"),
|
|
20
|
+
texts=[
|
|
21
|
+
TextConfig(
|
|
22
|
+
content="Hello World!",
|
|
23
|
+
y="50%",
|
|
24
|
+
font_size=96,
|
|
25
|
+
color="#e94560",
|
|
26
|
+
shadow=ShadowConfig(blur_radius=12),
|
|
27
|
+
)
|
|
28
|
+
],
|
|
29
|
+
output_dir="./output",
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
generate(post) # → ./output/post.png
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Platforms & formats
|
|
36
|
+
|
|
37
|
+
| Helper | Size |
|
|
38
|
+
|---|---|
|
|
39
|
+
| `instagram_post()` | 1080 × 1080 |
|
|
40
|
+
| `instagram_portrait()` | 1080 × 1350 |
|
|
41
|
+
| `instagram_story()` | 1080 × 1920 |
|
|
42
|
+
| `x_post()` | 1600 × 900 |
|
|
43
|
+
| `reddit_post()` | 1920 × 1080 |
|
|
44
|
+
| `blog_og()` | 1200 × 628 |
|
|
45
|
+
| `linkedin_post()` | 1080 × 1080 |
|
|
46
|
+
| `youtube_thumbnail()` | 1280 × 720 |
|
|
47
|
+
| `facebook_post()` | 1080 × 1080 |
|
|
48
|
+
| `tiktok_story()` | 1080 × 1920 |
|
|
49
|
+
|
|
50
|
+
Use `preset(Platform.CUSTOM, PostFormat.CUSTOM, width=800, height=600)` for custom sizes.
|
|
51
|
+
|
|
52
|
+
## Carousel / multi-image
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from postcanvas.models import CanvasConfig, BackgroundConfig
|
|
56
|
+
|
|
57
|
+
post = instagram_post(
|
|
58
|
+
canvases=[
|
|
59
|
+
CanvasConfig(background=BackgroundConfig(color="#e94560"),
|
|
60
|
+
texts=[TextConfig(content="Slide 1", ...)]),
|
|
61
|
+
CanvasConfig(background=BackgroundConfig(color="#0f3460"),
|
|
62
|
+
texts=[TextConfig(content="Slide 2", ...)]),
|
|
63
|
+
]
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Key model reference
|
|
68
|
+
|
|
69
|
+
### `PostConfig` (root)
|
|
70
|
+
| Field | Type | Description |
|
|
71
|
+
|---|---|---|
|
|
72
|
+
| `platform` | `Platform` | Target platform |
|
|
73
|
+
| `width` / `height` | `int` | Canvas size in px |
|
|
74
|
+
| `background` | `BackgroundConfig` | Global background |
|
|
75
|
+
| `padding` | `PaddingConfig` | Safe-area insets |
|
|
76
|
+
| `texts` | `List[TextConfig]` | Global text elements |
|
|
77
|
+
| `images` | `List[ImageElementConfig]` | Global image elements |
|
|
78
|
+
| `shapes` | `List[ShapeConfig]` | Global shapes |
|
|
79
|
+
| `canvases` | `List[CanvasConfig]` | Slides (carousel) |
|
|
80
|
+
| `watermark` | `WatermarkConfig` | Applied to every slide |
|
|
81
|
+
| `output_dir` | `str` | Where to save files |
|
|
82
|
+
| `output_format` | `OutputFormat` | `png` / `jpeg` / `webp` |
|
|
83
|
+
|
|
84
|
+
### Positioning
|
|
85
|
+
Every `x`, `y`, `width`, `height` accepts:
|
|
86
|
+
- **Absolute pixels**: `540`, `200`
|
|
87
|
+
- **Relative string**: `"50%"`, `"80%"`
|
|
88
|
+
|
|
89
|
+
### Anchors
|
|
90
|
+
`anchor` can be: `topleft`, `topcenter`, `topright`, `left`, `center`,
|
|
91
|
+
`right`, `bottomleft`, `bottomcenter`, `bottomright`
|
|
92
|
+
|
|
93
|
+
### z_index
|
|
94
|
+
Elements are composited in ascending `z_index` order across all types
|
|
95
|
+
(shapes, images, texts). Default values: shapes=1, images=5, texts=10.
|
|
96
|
+
|
|
97
|
+
### Text inside images and shapes
|
|
98
|
+
Both `ImageElementConfig` and `ShapeConfig` now support a `texts` list:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from postcanvas.models import ImageElementConfig, ShapeConfig, ShapeType, TextConfig
|
|
102
|
+
|
|
103
|
+
ShapeConfig(
|
|
104
|
+
type=ShapeType.ROUNDED_RECTANGLE,
|
|
105
|
+
x="50%", y="35%", width="70%", height="30%", anchor="center",
|
|
106
|
+
fill_color="#1f3b4d",
|
|
107
|
+
texts=[
|
|
108
|
+
TextConfig(content="Inside Shape", x="50%", y="50%", anchor="center")
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
ImageElementConfig(
|
|
113
|
+
src="assets/photo.jpg",
|
|
114
|
+
x="50%", y="70%", width="60%", height="35%", anchor="center",
|
|
115
|
+
texts=[
|
|
116
|
+
TextConfig(content="Inside Image", x="50%", y="88%", anchor="bottomcenter")
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Nested text coordinates are resolved relative to the element's own box, not the full canvas.
|
|
122
|
+
|
|
123
|
+
### Font inheritance (Post > Canvas > Text override)
|
|
124
|
+
You can define default text font at post level, override it per canvas, and still override per text:
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from postcanvas.presets import instagram_post
|
|
128
|
+
from postcanvas.models import CanvasConfig, TextConfig
|
|
129
|
+
|
|
130
|
+
post = instagram_post(
|
|
131
|
+
text_font_path="Roboto/static/Roboto-Regular.ttf", # default for whole post
|
|
132
|
+
texts=[
|
|
133
|
+
TextConfig(content="Uses post default", x="50%", y="15%"),
|
|
134
|
+
TextConfig(content="Custom text font", x="50%", y="25%", font_path="Roboto/static/Roboto-Bold.ttf"),
|
|
135
|
+
],
|
|
136
|
+
canvases=[
|
|
137
|
+
CanvasConfig(
|
|
138
|
+
text_font_path="Roboto/static/Roboto-Italic.ttf", # overrides post default on this slide
|
|
139
|
+
texts=[
|
|
140
|
+
TextConfig(content="Uses canvas override", x="50%", y="50%"),
|
|
141
|
+
TextConfig(content="Text-level still wins", x="50%", y="60%", font_path="Roboto/static/Roboto-Medium.ttf"),
|
|
142
|
+
],
|
|
143
|
+
)
|
|
144
|
+
],
|
|
145
|
+
)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Precedence: `TextConfig` > `CanvasConfig` > `PostConfig` > internal Arial fallback.
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""
|
|
2
|
+
postcanvas – Generate social-media images with Python.
|
|
3
|
+
|
|
4
|
+
Quick start
|
|
5
|
+
-----------
|
|
6
|
+
from postcanvas import generate
|
|
7
|
+
from postcanvas.presets import instagram_post
|
|
8
|
+
from postcanvas.models import BackgroundConfig, TextConfig, ShadowConfig
|
|
9
|
+
|
|
10
|
+
post = instagram_post(
|
|
11
|
+
background=BackgroundConfig(color="#1a1a2e"),
|
|
12
|
+
texts=[
|
|
13
|
+
TextConfig(
|
|
14
|
+
content="Hello World",
|
|
15
|
+
y="45%",
|
|
16
|
+
font_size=96,
|
|
17
|
+
color="#e94560",
|
|
18
|
+
shadow=ShadowConfig(blur_radius=12),
|
|
19
|
+
)
|
|
20
|
+
],
|
|
21
|
+
output_dir="./out",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
paths = generate(post)
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from .renderer import generate, render_one
|
|
28
|
+
from . import models, presets
|
|
29
|
+
|
|
30
|
+
__version__ = "0.1.0"
|
|
31
|
+
__all__ = ["generate", "render_one", "models", "presets"]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from .enums import (
|
|
2
|
+
Platform, PostFormat, GradientType, ImageFit, TextAlign, FontWeight,
|
|
3
|
+
ShapeType, BlendMode, OutputFormat, FilterType, TextTransform
|
|
4
|
+
)
|
|
5
|
+
from .primitives import (
|
|
6
|
+
ShadowConfig, StrokeConfig, GradientStop, GradientConfig,
|
|
7
|
+
PaddingConfig, BorderConfig, FilterConfig
|
|
8
|
+
)
|
|
9
|
+
from .background import BackgroundConfig
|
|
10
|
+
from .text import TextConfig
|
|
11
|
+
from .elements import ImageElementConfig, ShapeConfig
|
|
12
|
+
from .watermark import WatermarkConfig
|
|
13
|
+
from .meta import MetaConfig
|
|
14
|
+
from .canvas import CanvasConfig
|
|
15
|
+
from .post import PostConfig
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"Platform", "PostFormat", "GradientType", "ImageFit", "TextAlign",
|
|
19
|
+
"FontWeight", "ShapeType", "BlendMode", "OutputFormat", "FilterType",
|
|
20
|
+
"TextTransform", "ShadowConfig", "StrokeConfig", "GradientStop",
|
|
21
|
+
"GradientConfig", "PaddingConfig", "BorderConfig", "FilterConfig",
|
|
22
|
+
"BackgroundConfig", "TextConfig", "ImageElementConfig", "ShapeConfig",
|
|
23
|
+
"WatermarkConfig", "MetaConfig", "CanvasConfig", "PostConfig",
|
|
24
|
+
]
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from .enums import ImageFit
|
|
5
|
+
from .primitives import GradientConfig
|
|
6
|
+
|
|
7
|
+
class BackgroundConfig(BaseModel):
|
|
8
|
+
# Solid colour (hex / rgb() / rgba())
|
|
9
|
+
color: Optional[str] = None
|
|
10
|
+
|
|
11
|
+
# Gradient (overrides colour when set)
|
|
12
|
+
gradient: Optional[GradientConfig] = None
|
|
13
|
+
|
|
14
|
+
# Image source (local path or URL)
|
|
15
|
+
image_path: Optional[str] = None
|
|
16
|
+
image_url: Optional[str] = None
|
|
17
|
+
image_fit: ImageFit = ImageFit.COVER
|
|
18
|
+
image_opacity: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
19
|
+
image_blur: float = 0.0
|
|
20
|
+
|
|
21
|
+
# Colour overlay painted on top of the image
|
|
22
|
+
overlay_color: Optional[str] = None
|
|
23
|
+
overlay_opacity: float = Field(default=0.4, ge=0.0, le=1.0)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from .background import BackgroundConfig
|
|
5
|
+
from .text import TextConfig
|
|
6
|
+
from .elements import ImageElementConfig, ShapeConfig
|
|
7
|
+
from .watermark import WatermarkConfig
|
|
8
|
+
from .meta import MetaConfig
|
|
9
|
+
from .primitives import PaddingConfig, FilterConfig
|
|
10
|
+
|
|
11
|
+
class CanvasConfig(BaseModel):
|
|
12
|
+
"""
|
|
13
|
+
One slide / frame in a carousel (or the only frame for a single image).
|
|
14
|
+
|
|
15
|
+
Every field is optional – unset fields fall back to the parent PostConfig.
|
|
16
|
+
Elements (texts, images, shapes) are MERGED with the parent's by default;
|
|
17
|
+
set replace_elements=True to use ONLY the elements defined here.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
# Override parent dimensions
|
|
21
|
+
width: Optional[int] = None
|
|
22
|
+
height: Optional[int] = None
|
|
23
|
+
|
|
24
|
+
# Override parent background / padding
|
|
25
|
+
background: Optional[BackgroundConfig] = None
|
|
26
|
+
padding: Optional[PaddingConfig] = None
|
|
27
|
+
|
|
28
|
+
# Default text font for this canvas (overrides PostConfig defaults)
|
|
29
|
+
text_font_family: Optional[str] = None
|
|
30
|
+
text_font_path: Optional[str] = None
|
|
31
|
+
|
|
32
|
+
# Canvas-specific elements (merged with parent unless replace_elements=True)
|
|
33
|
+
texts: List[TextConfig] = Field(default_factory=list)
|
|
34
|
+
images: List[ImageElementConfig] = Field(default_factory=list)
|
|
35
|
+
shapes: List[ShapeConfig] = Field(default_factory=list)
|
|
36
|
+
|
|
37
|
+
# If True, these elements replace (not extend) the parent's element lists
|
|
38
|
+
replace_elements: bool = False
|
|
39
|
+
|
|
40
|
+
# Per-canvas post-processing
|
|
41
|
+
canvas_filters: List[FilterConfig] = Field(default_factory=list)
|
|
42
|
+
|
|
43
|
+
# Watermark override (None = use parent watermark)
|
|
44
|
+
watermark: Optional[WatermarkConfig] = None
|
|
45
|
+
|
|
46
|
+
# Output filename without extension (e.g. "slide_01")
|
|
47
|
+
output_filename: Optional[str] = None
|
|
48
|
+
|
|
49
|
+
meta: MetaConfig = Field(default_factory=MetaConfig)
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List, Optional, Tuple
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from .enums import ImageFit, BlendMode, ShapeType, Dimension
|
|
5
|
+
from .primitives import ShadowConfig, BorderConfig, FilterConfig, GradientConfig
|
|
6
|
+
from .text import TextConfig
|
|
7
|
+
|
|
8
|
+
class ImageElementConfig(BaseModel):
|
|
9
|
+
src: str # local path or URL
|
|
10
|
+
|
|
11
|
+
# Position & size
|
|
12
|
+
x: Dimension = "50%"
|
|
13
|
+
y: Dimension = "50%"
|
|
14
|
+
width: Optional[Dimension] = None # None = intrinsic
|
|
15
|
+
height: Optional[Dimension] = None
|
|
16
|
+
fit: ImageFit = ImageFit.CONTAIN
|
|
17
|
+
anchor: str = "center"
|
|
18
|
+
|
|
19
|
+
# Visual
|
|
20
|
+
border_radius: float = 0.0
|
|
21
|
+
opacity: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
22
|
+
rotation: float = 0.0
|
|
23
|
+
flip_horizontal: bool = False
|
|
24
|
+
flip_vertical: bool = False
|
|
25
|
+
|
|
26
|
+
# Adjustments (1.0 = neutral)
|
|
27
|
+
brightness: float = 1.0
|
|
28
|
+
contrast: float = 1.0
|
|
29
|
+
saturation: float = 1.0
|
|
30
|
+
|
|
31
|
+
# Decorations
|
|
32
|
+
shadow: Optional[ShadowConfig] = None
|
|
33
|
+
border: Optional[BorderConfig] = None
|
|
34
|
+
texts: List[TextConfig] = Field(default_factory=list)
|
|
35
|
+
|
|
36
|
+
# Compositing
|
|
37
|
+
blend_mode: BlendMode = BlendMode.NORMAL
|
|
38
|
+
z_index: int = 5
|
|
39
|
+
visible: bool = True
|
|
40
|
+
filters: List[FilterConfig] = Field(default_factory=list)
|
|
41
|
+
|
|
42
|
+
class ShapeConfig(BaseModel):
|
|
43
|
+
type: ShapeType = ShapeType.RECTANGLE
|
|
44
|
+
|
|
45
|
+
# Position & size
|
|
46
|
+
x: Dimension = 0
|
|
47
|
+
y: Dimension = 0
|
|
48
|
+
width: Dimension = 100
|
|
49
|
+
height: Dimension = 100
|
|
50
|
+
anchor: str = "topleft"
|
|
51
|
+
|
|
52
|
+
# Fill
|
|
53
|
+
fill_color: Optional[str] = None
|
|
54
|
+
fill_gradient: Optional[GradientConfig] = None
|
|
55
|
+
|
|
56
|
+
# Stroke
|
|
57
|
+
stroke_color: Optional[str] = None
|
|
58
|
+
stroke_width: int = 0
|
|
59
|
+
|
|
60
|
+
# Shape-specific
|
|
61
|
+
border_radius: float = 0.0 # ROUNDED_RECTANGLE
|
|
62
|
+
sides: int = 6 # POLYGON (regular)
|
|
63
|
+
star_points: int = 5 # STAR
|
|
64
|
+
star_inner_r: float = 0.4 # STAR inner ratio
|
|
65
|
+
points: Optional[List[Tuple[float, float]]] = None # POLYGON custom
|
|
66
|
+
|
|
67
|
+
# Compositing
|
|
68
|
+
opacity: float = Field(default=1.0, ge=0.0, le=1.0)
|
|
69
|
+
rotation: float = 0.0
|
|
70
|
+
blend_mode: BlendMode = BlendMode.NORMAL
|
|
71
|
+
z_index: int = 1
|
|
72
|
+
visible: bool = True
|
|
73
|
+
shadow: Optional[ShadowConfig] = None
|
|
74
|
+
texts: List[TextConfig] = Field(default_factory=list)
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
class Platform(str, Enum):
|
|
4
|
+
INSTAGRAM = "instagram"
|
|
5
|
+
X = "x"
|
|
6
|
+
TWITTER = "twitter"
|
|
7
|
+
REDDIT = "reddit"
|
|
8
|
+
BLOG = "blog"
|
|
9
|
+
LINKEDIN = "linkedin"
|
|
10
|
+
FACEBOOK = "facebook"
|
|
11
|
+
TIKTOK = "tiktok"
|
|
12
|
+
YOUTUBE = "youtube"
|
|
13
|
+
CUSTOM = "custom"
|
|
14
|
+
|
|
15
|
+
class PostFormat(str, Enum):
|
|
16
|
+
SQUARE = "square"
|
|
17
|
+
PORTRAIT = "portrait"
|
|
18
|
+
LANDSCAPE = "landscape"
|
|
19
|
+
STORY = "story"
|
|
20
|
+
COVER = "cover"
|
|
21
|
+
BANNER = "banner"
|
|
22
|
+
CUSTOM = "custom"
|
|
23
|
+
|
|
24
|
+
class GradientType(str, Enum):
|
|
25
|
+
LINEAR = "linear"
|
|
26
|
+
RADIAL = "radial"
|
|
27
|
+
CONIC = "conic"
|
|
28
|
+
|
|
29
|
+
class ImageFit(str, Enum):
|
|
30
|
+
COVER = "cover"
|
|
31
|
+
CONTAIN = "contain"
|
|
32
|
+
FILL = "fill"
|
|
33
|
+
CENTER = "center"
|
|
34
|
+
|
|
35
|
+
class TextAlign(str, Enum):
|
|
36
|
+
LEFT = "left"
|
|
37
|
+
CENTER = "center"
|
|
38
|
+
RIGHT = "right"
|
|
39
|
+
JUSTIFY = "justify"
|
|
40
|
+
|
|
41
|
+
class FontWeight(str, Enum):
|
|
42
|
+
THIN = "thin"
|
|
43
|
+
LIGHT = "light"
|
|
44
|
+
REGULAR = "regular"
|
|
45
|
+
MEDIUM = "medium"
|
|
46
|
+
SEMIBOLD = "semibold"
|
|
47
|
+
BOLD = "bold"
|
|
48
|
+
EXTRABOLD = "extrabold"
|
|
49
|
+
BLACK = "black"
|
|
50
|
+
|
|
51
|
+
class ShapeType(str, Enum):
|
|
52
|
+
RECTANGLE = "rectangle"
|
|
53
|
+
CIRCLE = "circle"
|
|
54
|
+
ELLIPSE = "ellipse"
|
|
55
|
+
LINE = "line"
|
|
56
|
+
ROUNDED_RECTANGLE = "rounded_rectangle"
|
|
57
|
+
TRIANGLE = "triangle"
|
|
58
|
+
POLYGON = "polygon"
|
|
59
|
+
STAR = "star"
|
|
60
|
+
|
|
61
|
+
class BlendMode(str, Enum):
|
|
62
|
+
NORMAL = "normal"
|
|
63
|
+
MULTIPLY = "multiply"
|
|
64
|
+
SCREEN = "screen"
|
|
65
|
+
OVERLAY = "overlay"
|
|
66
|
+
DARKEN = "darken"
|
|
67
|
+
LIGHTEN = "lighten"
|
|
68
|
+
DIFFERENCE = "difference"
|
|
69
|
+
EXCLUSION = "exclusion"
|
|
70
|
+
|
|
71
|
+
class OutputFormat(str, Enum):
|
|
72
|
+
PNG = "png"
|
|
73
|
+
JPEG = "jpeg"
|
|
74
|
+
JPG = "jpg"
|
|
75
|
+
WEBP = "webp"
|
|
76
|
+
|
|
77
|
+
class FilterType(str, Enum):
|
|
78
|
+
NONE = "none"
|
|
79
|
+
BLUR = "blur"
|
|
80
|
+
SHARPEN = "sharpen"
|
|
81
|
+
GRAYSCALE = "grayscale"
|
|
82
|
+
SEPIA = "sepia"
|
|
83
|
+
BRIGHTNESS = "brightness"
|
|
84
|
+
CONTRAST = "contrast"
|
|
85
|
+
SATURATION = "saturation"
|
|
86
|
+
INVERT = "invert"
|
|
87
|
+
VIGNETTE = "vignette"
|
|
88
|
+
|
|
89
|
+
class TextTransform(str, Enum):
|
|
90
|
+
NONE = "none"
|
|
91
|
+
UPPERCASE = "uppercase"
|
|
92
|
+
LOWERCASE = "lowercase"
|
|
93
|
+
CAPITALIZE = "capitalize"
|
|
94
|
+
|
|
95
|
+
# Dimension type: int/float = absolute px, str "50%" = relative
|
|
96
|
+
Dimension = int | float | str
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import Any, Dict, List, Optional
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
|
|
5
|
+
class MetaConfig(BaseModel):
|
|
6
|
+
"""Non-rendered metadata – useful for tracking, CMS integration, etc."""
|
|
7
|
+
title: Optional[str] = None
|
|
8
|
+
description: Optional[str] = None
|
|
9
|
+
tags: List[str] = Field(default_factory=list)
|
|
10
|
+
author: Optional[str] = None
|
|
11
|
+
created_at: Optional[str] = None
|
|
12
|
+
custom: Dict[str, Any] = Field(default_factory=dict)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from .enums import Platform, PostFormat, OutputFormat
|
|
5
|
+
from .background import BackgroundConfig
|
|
6
|
+
from .text import TextConfig
|
|
7
|
+
from .elements import ImageElementConfig, ShapeConfig
|
|
8
|
+
from .watermark import WatermarkConfig
|
|
9
|
+
from .meta import MetaConfig
|
|
10
|
+
from .primitives import PaddingConfig, FilterConfig
|
|
11
|
+
from .canvas import CanvasConfig
|
|
12
|
+
|
|
13
|
+
class PostConfig(BaseModel):
|
|
14
|
+
"""
|
|
15
|
+
Root configuration for a social-media post.
|
|
16
|
+
|
|
17
|
+
All settings here act as DEFAULTS for every canvas.
|
|
18
|
+
A CanvasConfig inside `canvases` can override any of them.
|
|
19
|
+
|
|
20
|
+
Single image → leave `canvases` empty.
|
|
21
|
+
Carousel → populate `canvases` (one entry per slide).
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
# ── Platform & format preset ─────────────────────────────────────────────
|
|
25
|
+
platform: Platform = Platform.INSTAGRAM
|
|
26
|
+
format: PostFormat = PostFormat.SQUARE
|
|
27
|
+
|
|
28
|
+
# ── Canvas dimensions ────────────────────────────────────────────────────
|
|
29
|
+
width: int = 1080
|
|
30
|
+
height: int = 1080
|
|
31
|
+
|
|
32
|
+
# ── Background (default for all slides) ──────────────────────────────────
|
|
33
|
+
background: BackgroundConfig = Field(default_factory=BackgroundConfig)
|
|
34
|
+
|
|
35
|
+
# ── Safe-area padding ────────────────────────────────────────────────────
|
|
36
|
+
padding: PaddingConfig = Field(default_factory=PaddingConfig)
|
|
37
|
+
|
|
38
|
+
# ── Default text font (can be overridden by canvas/text) ───────────────
|
|
39
|
+
text_font_family: Optional[str] = None
|
|
40
|
+
text_font_path: Optional[str] = None
|
|
41
|
+
|
|
42
|
+
# ── Global elements (appear on EVERY slide unless overridden) ─────────────
|
|
43
|
+
texts: List[TextConfig] = Field(default_factory=list)
|
|
44
|
+
images: List[ImageElementConfig] = Field(default_factory=list)
|
|
45
|
+
shapes: List[ShapeConfig] = Field(default_factory=list)
|
|
46
|
+
|
|
47
|
+
# ── Slides (carousel / multi-image) ──────────────────────────────────────
|
|
48
|
+
canvases: List[CanvasConfig] = Field(default_factory=list)
|
|
49
|
+
|
|
50
|
+
# ── Post-processing ──────────────────────────────────────────────────────
|
|
51
|
+
canvas_filters: List[FilterConfig] = Field(default_factory=list)
|
|
52
|
+
watermark: Optional[WatermarkConfig] = None
|
|
53
|
+
|
|
54
|
+
# ── Output ───────────────────────────────────────────────────────────────
|
|
55
|
+
output_dir: str = "./output"
|
|
56
|
+
output_format: OutputFormat = OutputFormat.PNG
|
|
57
|
+
output_filename: str = "post"
|
|
58
|
+
quality: int = Field(default=95, ge=1, le=100)
|
|
59
|
+
dpi: int = 96
|
|
60
|
+
|
|
61
|
+
# ── Metadata ─────────────────────────────────────────────────────────────
|
|
62
|
+
meta: MetaConfig = Field(default_factory=MetaConfig)
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def slide_count(self) -> int:
|
|
66
|
+
return max(1, len(self.canvases))
|