1 # Quote Cards
2
3 Generate visually appealing quote card images for social media from podcast pull quotes.
4
5 ## Table of Contents
6
7 - [Design Specifications](#design-specifications)
8 - [Output Dimensions](#output-dimensions)
9
10 - [Generation Methods](#generation-methods)
11 - [Text Layout Rules](#text-layout-rules)
12
13 ## Design Specifications
14
15 Each quote card should include:
16
17 1. **Quote text** — formatted for visual impact with line breaks at natural pauses
18 2. **Speaker attribution** — name and title/role
19
20 3. **Podcast branding** — show name or logo
21 4. **Background** — gradient, solid color, or subtle pattern (not a busy photo)
22
23 ### Style Guidelines
24
25 - **Typography**: Use a bold, readable font for the quote. Sans-serif for modern feel, serif for authority/editorial feel.
26 - **Contrast**: Ensure strong contrast between text and background (WCAG AA minimum).
27
28 - **Whitespace**: Generous margins (at least 10% of canvas on all sides). The quote should breathe.
29 - **Hierarchy**: Quote text is largest, attribution is smaller, branding is smallest.
30
31 - **Color**: Pull from the podcast's brand palette if available. If not, use the tone of the episode to guide color choice (warm tones for personal stories, cool tones for technical content, bold tones for controversial takes).
32
33 ## Output Dimensions
34
35 Generate each quote card in three sizes:
36
37 | Platform | Dimensions | Aspect Ratio | Use |
38
39 |----------|-----------|-------------|-----|
40
41 | Instagram Feed | 1080 x 1080 | 1:1 | Feed posts, carousels |
42
43 | Twitter/LinkedIn | 1200 x 675 | 16:9 | Tweet images, LinkedIn posts |
44
45 | Stories/Reels | 1080 x 1920 | 9:16 | Instagram Stories, TikTok |
46
47 ## Generation Methods
48
49 ### Method 1: Pillow (Python)
50
51 Use Pillow for programmatic generation. Best for batch processing and consistent branding.
52
53 ```python
54
55 from PIL import Image, ImageDraw, ImageFont
56
57 import textwrap
58
59 def generate_quote_card(
60
61 quote_text: str,
62
63 speaker_name: str,
64
65 speaker_title: str,
66
67 podcast_name: str,
68
69 output_path: str,
70
71 width: int = 1080,
72
73 height: int = 1080,
74
75 bg_color: str = "#1a1a2e",
76
77 accent_color: str = "#e94560",
78
79 text_color: str = "#ffffff",
80
81 subtitle_color: str = "#a0a0b0",
82
83 ):
84
85 img = Image.new("RGB", (width, height), bg_color)
86
87 draw = ImageDraw.Draw(img)
88
89 margin_x = int(width * 0.12)
90
91 margin_y = int(height * 0.15)
92
93 max_text_width = width - 2 * margin_x
94
95 try:
96
97 font_quote = ImageFont.truetype(
98
99 "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
100
101 int(width * 0.042),
102
103 )
104
105 font_speaker = ImageFont.truetype(
106
107 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
108
109 int(width * 0.028),
110
111 )
112
113 font_brand = ImageFont.truetype(
114
115 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
116
117 int(width * 0.022),
118
119 )
120
121 except OSError:
122
123 font_quote = ImageFont.load_default()
124
125 font_speaker = font_quote
126
127 font_brand = font_quote
128
129 draw.text(
130
131 (margin_x, margin_y - int(height * 0.08)),
132
133 "\u201c",
134
135 fill=accent_color,
136
137 font=ImageFont.truetype(
138
139 "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf",
140
141 int(width * 0.12),
142
143 ) if font_quote != ImageFont.load_default() else font_quote,
144
145 )
146
147 chars_per_line = int(max_text_width / (width * 0.025))
148
149 wrapped = textwrap.fill(quote_text, width=chars_per_line)
150
151 lines = wrapped.split("\n")
152
153 y = margin_y + int(height * 0.05)
154
155 line_height = int(width * 0.058)
156
157 for line in lines:
158
159 draw.text((margin_x, y), line, fill=text_color, font=font_quote)
160
161 y += line_height
162
163 y += int(height * 0.04)
164
165 draw.line(
166
167 [(margin_x, y), (margin_x + int(width * 0.08), y)],
168
169 fill=accent_color,
170
171 width=3,
172
173 )
174
175 y += int(height * 0.03)
176
177 draw.text((margin_x, y), speaker_name, fill=text_color, font=font_speaker)
178
179 y += int(height * 0.04)
180
181 if speaker_title:
182
183 draw.text(
184
185 (margin_x, y), speaker_title, fill=subtitle_color, font=font_speaker
186
187 )
188
189 y += int(height * 0.04)
190
191 brand_bbox = draw.textbbox((0, 0), podcast_name, font=font_brand)
192
193 brand_width = brand_bbox[2] - brand_bbox[0]
194
195 draw.text(
196
197 (width - margin_x - brand_width, height - margin_y),
198
199 podcast_name,
200
201 fill=subtitle_color,
202
203 font=font_brand,
204
205 )
206
207 img.save(output_path, quality=95)
208
209 return output_path
210
211 ```
212
213 ### Method 2: AI Image Generation
214
215 For higher visual quality, use the `generate_image` tool to create unique, branded quote card backgrounds, then overlay the text programmatically. This produces more visually distinctive cards but takes longer.
216
217 Prompt pattern:
218
219 ```text
220
221 Abstract background for a quote card. [Color palette description].
222
223 Subtle gradient with soft geometric shapes. No text. Modern, clean, editorial feel.
224
225 Dimensions: [width]x[height].
226
227 ```
228
229 ## Text Layout Rules
230
231 - **Maximum quote length**: 150 characters for single-size cards. For longer quotes (up to 250 chars), reduce font size by 20%.
232 - **Line breaks**: Break at natural pauses, not mid-phrase. Prioritize semantic breaks:
233
234 - Good: "Culture eats strategy\nfor breakfast"
235 - Bad: "Culture eats\nstrategy for breakfast"
236
237 - **Emphasis**: Use ALL CAPS for one key word/phrase if the quote has a clear punchline. Don't overdo it.
238 - **Attribution format**: "— Speaker Name" on one line, title/role on the next.