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.
Files changed (33) hide show
  1. postcanvas-0.1.0/PKG-INFO +165 -0
  2. postcanvas-0.1.0/README.md +148 -0
  3. postcanvas-0.1.0/postcanvas/__init__.py +31 -0
  4. postcanvas-0.1.0/postcanvas/models/__init__.py +24 -0
  5. postcanvas-0.1.0/postcanvas/models/background.py +23 -0
  6. postcanvas-0.1.0/postcanvas/models/canvas.py +49 -0
  7. postcanvas-0.1.0/postcanvas/models/elements.py +74 -0
  8. postcanvas-0.1.0/postcanvas/models/enums.py +96 -0
  9. postcanvas-0.1.0/postcanvas/models/meta.py +12 -0
  10. postcanvas-0.1.0/postcanvas/models/post.py +66 -0
  11. postcanvas-0.1.0/postcanvas/models/primitives.py +57 -0
  12. postcanvas-0.1.0/postcanvas/models/text.py +54 -0
  13. postcanvas-0.1.0/postcanvas/models/watermark.py +15 -0
  14. postcanvas-0.1.0/postcanvas/presets/__init__.py +22 -0
  15. postcanvas-0.1.0/postcanvas/presets/platforms.py +64 -0
  16. postcanvas-0.1.0/postcanvas/renderer/__init__.py +2 -0
  17. postcanvas-0.1.0/postcanvas/renderer/background.py +42 -0
  18. postcanvas-0.1.0/postcanvas/renderer/canvas.py +163 -0
  19. postcanvas-0.1.0/postcanvas/renderer/filters.py +49 -0
  20. postcanvas-0.1.0/postcanvas/renderer/gradient.py +43 -0
  21. postcanvas-0.1.0/postcanvas/renderer/images.py +120 -0
  22. postcanvas-0.1.0/postcanvas/renderer/loader.py +37 -0
  23. postcanvas-0.1.0/postcanvas/renderer/shapes.py +123 -0
  24. postcanvas-0.1.0/postcanvas/renderer/text.py +226 -0
  25. postcanvas-0.1.0/postcanvas/renderer/utils.py +62 -0
  26. postcanvas-0.1.0/postcanvas/utils/__init__.py +1 -0
  27. postcanvas-0.1.0/postcanvas.egg-info/PKG-INFO +165 -0
  28. postcanvas-0.1.0/postcanvas.egg-info/SOURCES.txt +31 -0
  29. postcanvas-0.1.0/postcanvas.egg-info/dependency_links.txt +1 -0
  30. postcanvas-0.1.0/postcanvas.egg-info/requires.txt +9 -0
  31. postcanvas-0.1.0/postcanvas.egg-info/top_level.txt +1 -0
  32. postcanvas-0.1.0/pyproject.toml +25 -0
  33. 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))