quickthumb 0.3.1__tar.gz → 0.5.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 (379) hide show
  1. quickthumb-0.5.0/.claude/skills/next-task/SKILL.md +15 -0
  2. {quickthumb-0.3.1 → quickthumb-0.5.0}/.claude/skills/tdd/SKILL.md +1 -2
  3. {quickthumb-0.3.1 → quickthumb-0.5.0}/.github/workflows/docs.yml +4 -0
  4. {quickthumb-0.3.1 → quickthumb-0.5.0}/.github/workflows/publish.yaml +1 -1
  5. {quickthumb-0.3.1 → quickthumb-0.5.0}/PKG-INFO +109 -16
  6. {quickthumb-0.3.1 → quickthumb-0.5.0}/README.md +105 -15
  7. quickthumb-0.5.0/assets/brand/apple-touch-icon.png +0 -0
  8. quickthumb-0.5.0/assets/brand/favicon.ico +0 -0
  9. quickthumb-0.5.0/assets/brand/favicon.png +0 -0
  10. quickthumb-0.5.0/assets/brand/quickthumb-icon-1024.png +0 -0
  11. quickthumb-0.5.0/assets/brand/quickthumb-icon-152.png +0 -0
  12. quickthumb-0.5.0/assets/brand/quickthumb-icon-16.png +0 -0
  13. quickthumb-0.5.0/assets/brand/quickthumb-icon-167.png +0 -0
  14. quickthumb-0.5.0/assets/brand/quickthumb-icon-180.png +0 -0
  15. quickthumb-0.5.0/assets/brand/quickthumb-icon-192.png +0 -0
  16. quickthumb-0.5.0/assets/brand/quickthumb-icon-256.png +0 -0
  17. quickthumb-0.5.0/assets/brand/quickthumb-icon-32.png +0 -0
  18. quickthumb-0.5.0/assets/brand/quickthumb-icon-48.png +0 -0
  19. quickthumb-0.5.0/assets/brand/quickthumb-icon-512.png +0 -0
  20. quickthumb-0.5.0/assets/brand/quickthumb-icon-64.png +0 -0
  21. quickthumb-0.5.0/assets/brand/quickthumb-icon-master-transparent.png +0 -0
  22. quickthumb-0.5.0/assets/brand/quickthumb-icon-master.png +0 -0
  23. quickthumb-0.5.0/assets/brand/social-preview.png +0 -0
  24. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/background.md +6 -2
  25. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/canvas.md +4 -0
  26. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/effects.md +52 -0
  27. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/enums.md +53 -0
  28. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/image.md +5 -1
  29. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/index.md +9 -3
  30. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/outline.md +4 -0
  31. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/shape.md +4 -0
  32. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/api/text.md +57 -0
  33. quickthumb-0.5.0/docs/assets/brand/apple-touch-icon.png +0 -0
  34. quickthumb-0.5.0/docs/assets/brand/favicon.ico +0 -0
  35. quickthumb-0.5.0/docs/assets/brand/favicon.png +0 -0
  36. quickthumb-0.5.0/docs/assets/brand/quickthumb-icon-192.png +0 -0
  37. quickthumb-0.5.0/docs/assets/brand/quickthumb-icon-512.png +0 -0
  38. quickthumb-0.5.0/docs/assets/brand/quickthumb-icon.png +0 -0
  39. quickthumb-0.5.0/docs/assets/brand/social-preview.png +0 -0
  40. quickthumb-0.5.0/docs/assets/examples/instagram_news_card.png +0 -0
  41. quickthumb-0.5.0/docs/assets/examples/podcast_interview_promo.png +0 -0
  42. quickthumb-0.5.0/docs/assets/examples/shorts_cover_agent.png +0 -0
  43. quickthumb-0.5.0/docs/assets/examples/youtube_reaction.png +0 -0
  44. quickthumb-0.5.0/docs/assets/examples/youtube_talking_head.png +0 -0
  45. {quickthumb-0.3.1 → quickthumb-0.5.0/docs/assets}/examples/youtube_thumbnail_02.png +0 -0
  46. quickthumb-0.5.0/docs/assets/examples/youtube_tutorial_explainer.png +0 -0
  47. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/changelog.md +4 -0
  48. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/concepts.md +5 -1
  49. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/cookbook/ai-workflow.md +10 -6
  50. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/cookbook/index.md +7 -3
  51. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/cookbook/instagram-card.md +4 -0
  52. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/cookbook/podcast-promo.md +5 -1
  53. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/cookbook/shorts-cover.md +4 -0
  54. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/cookbook/webfonts-rembg.md +6 -2
  55. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/cookbook/youtube-thumbnail.md +6 -2
  56. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/faq.md +6 -2
  57. quickthumb-0.5.0/docs/favicon.ico +0 -0
  58. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/getting-started.md +6 -2
  59. quickthumb-0.5.0/docs/googleafa40cbb47cddae5.html +1 -0
  60. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/index.md +13 -2
  61. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/installation.md +9 -4
  62. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/json-schema.md +56 -7
  63. quickthumb-0.5.0/docs/overrides/main.html +31 -0
  64. quickthumb-0.5.0/docs/robots.txt +4 -0
  65. quickthumb-0.5.0/docs/site.webmanifest +19 -0
  66. quickthumb-0.5.0/docs/stylesheets/extra.css +452 -0
  67. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/README.md +3 -3
  68. quickthumb-0.5.0/examples/instagram_news_card.png +0 -0
  69. quickthumb-0.5.0/examples/podcast_interview_promo.png +0 -0
  70. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/shorts_cover_agent.json +3 -2
  71. quickthumb-0.5.0/examples/shorts_cover_agent.png +0 -0
  72. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/shorts_cover_agent.py +1 -1
  73. quickthumb-0.5.0/examples/youtube_reaction.png +0 -0
  74. quickthumb-0.5.0/examples/youtube_talking_head.png +0 -0
  75. {quickthumb-0.3.1/docs/assets → quickthumb-0.5.0}/examples/youtube_thumbnail_02.png +0 -0
  76. quickthumb-0.5.0/examples/youtube_tutorial_explainer.png +0 -0
  77. {quickthumb-0.3.1 → quickthumb-0.5.0}/mkdocs.yml +8 -1
  78. quickthumb-0.5.0/overrides/main.html +9 -0
  79. quickthumb-0.5.0/overrides/partials/header.html +80 -0
  80. {quickthumb-0.3.1 → quickthumb-0.5.0}/pyproject.toml +8 -1
  81. {quickthumb-0.3.1 → quickthumb-0.5.0}/quickthumb/__init__.py +6 -1
  82. quickthumb-0.5.0/quickthumb/_entry.py +18 -0
  83. {quickthumb-0.3.1 → quickthumb-0.5.0}/quickthumb/canvas.py +645 -83
  84. quickthumb-0.5.0/quickthumb/cli.py +200 -0
  85. {quickthumb-0.3.1 → quickthumb-0.5.0}/quickthumb/models.py +68 -19
  86. quickthumb-0.5.0/quickthumb/templates/instagram-square.json +18 -0
  87. quickthumb-0.5.0/quickthumb/templates/og-image.json +18 -0
  88. quickthumb-0.5.0/quickthumb/templates/twitter-card.json +18 -0
  89. quickthumb-0.5.0/quickthumb/templates/youtube-16x9.json +18 -0
  90. quickthumb-0.5.0/specs/TASKS.md +86 -0
  91. quickthumb-0.5.0/tests/snapshots/auto_scale_rich_text.png +0 -0
  92. quickthumb-0.5.0/tests/snapshots/auto_scale_simple_text.png +0 -0
  93. quickthumb-0.5.0/tests/snapshots/custom_canvas_layer.png +0 -0
  94. quickthumb-0.5.0/tests/snapshots/custom_layer_with_kwargs.png +0 -0
  95. quickthumb-0.5.0/tests/snapshots/explicit_newlines_preserved_with_max_width.png +0 -0
  96. quickthumb-0.5.0/tests/snapshots/grain_effect_on_background.png +0 -0
  97. quickthumb-0.5.0/tests/snapshots/image_overlay_fit_contain.png +0 -0
  98. quickthumb-0.5.0/tests/snapshots/image_overlay_fit_cover.png +0 -0
  99. quickthumb-0.5.0/tests/snapshots/image_overlay_fit_fill.png +0 -0
  100. quickthumb-0.5.0/tests/snapshots/image_with_border_radius.png +0 -0
  101. quickthumb-0.5.0/tests/snapshots/image_with_rotation.png +0 -0
  102. quickthumb-0.5.0/tests/snapshots/image_with_text_overlay.png +0 -0
  103. quickthumb-0.5.0/tests/snapshots/jpeg_format.jpg +0 -0
  104. quickthumb-0.5.0/tests/snapshots/multiline_text_with_background.png +0 -0
  105. quickthumb-0.5.0/tests/snapshots/multiline_text_with_fill.png +0 -0
  106. quickthumb-0.5.0/tests/snapshots/multiple_images.png +0 -0
  107. quickthumb-0.5.0/tests/snapshots/percentage_with_alignment.png +0 -0
  108. quickthumb-0.5.0/tests/snapshots/rich_text_advanced_styles.png +0 -0
  109. quickthumb-0.5.0/tests/snapshots/rich_text_alignment.png +0 -0
  110. quickthumb-0.5.0/tests/snapshots/rich_text_different_colors.png +0 -0
  111. quickthumb-0.5.0/tests/snapshots/rich_text_mixed_fonts.png +0 -0
  112. quickthumb-0.5.0/tests/snapshots/rich_text_mixed_styles.png +0 -0
  113. quickthumb-0.5.0/tests/snapshots/rich_text_with_background.png +0 -0
  114. quickthumb-0.5.0/tests/snapshots/rich_text_with_effects.png +0 -0
  115. quickthumb-0.5.0/tests/snapshots/rich_text_with_per_part_fill.png +0 -0
  116. quickthumb-0.5.0/tests/snapshots/rich_text_wrapping.png +0 -0
  117. quickthumb-0.5.0/tests/snapshots/roboto_bold.png +0 -0
  118. quickthumb-0.5.0/tests/snapshots/roboto_bold_italic.png +0 -0
  119. quickthumb-0.5.0/tests/snapshots/roboto_italic.png +0 -0
  120. quickthumb-0.5.0/tests/snapshots/shape_ellipse_basic.png +0 -0
  121. quickthumb-0.5.0/tests/snapshots/shape_rectangle_with_border_radius.png +0 -0
  122. quickthumb-0.5.0/tests/snapshots/shape_with_effects.png +0 -0
  123. quickthumb-0.5.0/tests/snapshots/shape_with_rotation.png +0 -0
  124. quickthumb-0.5.0/tests/snapshots/text_alignment.png +0 -0
  125. quickthumb-0.5.0/tests/snapshots/text_bold_and_italic.png +0 -0
  126. quickthumb-0.5.0/tests/snapshots/text_fill_with_letter_spacing.png +0 -0
  127. quickthumb-0.5.0/tests/snapshots/text_fill_with_stroke.png +0 -0
  128. quickthumb-0.5.0/tests/snapshots/text_no_wrapping.png +0 -0
  129. quickthumb-0.5.0/tests/snapshots/text_rendering.png +0 -0
  130. quickthumb-0.5.0/tests/snapshots/text_rotation_basic.png +0 -0
  131. quickthumb-0.5.0/tests/snapshots/text_rotation_centered.png +0 -0
  132. quickthumb-0.5.0/tests/snapshots/text_rotation_with_effects.png +0 -0
  133. quickthumb-0.5.0/tests/snapshots/text_weight_black.png +0 -0
  134. quickthumb-0.5.0/tests/snapshots/text_weight_light.png +0 -0
  135. quickthumb-0.5.0/tests/snapshots/text_with_background_and_effects.png +0 -0
  136. quickthumb-0.5.0/tests/snapshots/text_with_background_basic.png +0 -0
  137. quickthumb-0.5.0/tests/snapshots/text_with_basic_shadow.png +0 -0
  138. quickthumb-0.5.0/tests/snapshots/text_with_blurred_shadow.png +0 -0
  139. quickthumb-0.5.0/tests/snapshots/text_with_glow.png +0 -0
  140. quickthumb-0.5.0/tests/snapshots/text_with_image_fill.png +0 -0
  141. quickthumb-0.5.0/tests/snapshots/text_with_line_height.png +0 -0
  142. quickthumb-0.5.0/tests/snapshots/text_with_linear_gradient_fill.png +0 -0
  143. quickthumb-0.5.0/tests/snapshots/text_with_multiple_glows.png +0 -0
  144. quickthumb-0.5.0/tests/snapshots/text_with_radial_gradient_fill.png +0 -0
  145. quickthumb-0.5.0/tests/snapshots/text_with_stroke.png +0 -0
  146. quickthumb-0.5.0/tests/snapshots/text_wrapping_center_aligned.png +0 -0
  147. quickthumb-0.5.0/tests/snapshots/text_wrapping_center_aligned_percentage.png +0 -0
  148. quickthumb-0.5.0/tests/snapshots/text_wrapping_left_aligned.png +0 -0
  149. quickthumb-0.5.0/tests/snapshots/text_wrapping_long_word_overflow.png +0 -0
  150. quickthumb-0.5.0/tests/snapshots/text_wrapping_right_aligned.png +0 -0
  151. quickthumb-0.5.0/tests/snapshots/webfont_basic_rendering.png +0 -0
  152. quickthumb-0.5.0/tests/snapshots/webp_format.webp +0 -0
  153. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/test_background_layers.py +134 -12
  154. quickthumb-0.5.0/tests/test_canvas.py +629 -0
  155. quickthumb-0.5.0/tests/test_cli.py +337 -0
  156. quickthumb-0.5.0/tests/test_docs_seo.py +55 -0
  157. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/test_rendering.py +491 -9
  158. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/test_text_layers.py +161 -0
  159. {quickthumb-0.3.1 → quickthumb-0.5.0}/uv.lock +158 -2
  160. quickthumb-0.3.1/CLAUDE.md +0 -30
  161. quickthumb-0.3.1/DESIGN.md +0 -573
  162. quickthumb-0.3.1/PROGRESS.md +0 -125
  163. quickthumb-0.3.1/docs/assets/examples/instagram_news_card.png +0 -0
  164. quickthumb-0.3.1/docs/assets/examples/podcast_interview_promo.png +0 -0
  165. quickthumb-0.3.1/docs/assets/examples/shorts_cover_agent.png +0 -0
  166. quickthumb-0.3.1/docs/assets/examples/youtube_reaction.png +0 -0
  167. quickthumb-0.3.1/docs/assets/examples/youtube_talking_head.png +0 -0
  168. quickthumb-0.3.1/docs/assets/examples/youtube_tutorial_explainer.png +0 -0
  169. quickthumb-0.3.1/docs/stylesheets/extra.css +0 -83
  170. quickthumb-0.3.1/examples/instagram_news_card.png +0 -0
  171. quickthumb-0.3.1/examples/podcast_interview_promo.png +0 -0
  172. quickthumb-0.3.1/examples/shorts_cover_agent.png +0 -0
  173. quickthumb-0.3.1/examples/youtube_reaction.png +0 -0
  174. quickthumb-0.3.1/examples/youtube_talking_head.png +0 -0
  175. quickthumb-0.3.1/examples/youtube_tutorial_explainer.png +0 -0
  176. quickthumb-0.3.1/tests/snapshots/auto_scale_rich_text.png +0 -0
  177. quickthumb-0.3.1/tests/snapshots/auto_scale_simple_text.png +0 -0
  178. quickthumb-0.3.1/tests/snapshots/custom_canvas_layer.png +0 -0
  179. quickthumb-0.3.1/tests/snapshots/explicit_newlines_preserved_with_max_width.png +0 -0
  180. quickthumb-0.3.1/tests/snapshots/image_overlay_fit_contain.png +0 -0
  181. quickthumb-0.3.1/tests/snapshots/image_overlay_fit_cover.png +0 -0
  182. quickthumb-0.3.1/tests/snapshots/image_overlay_fit_fill.png +0 -0
  183. quickthumb-0.3.1/tests/snapshots/image_with_border_radius.png +0 -0
  184. quickthumb-0.3.1/tests/snapshots/image_with_rotation.png +0 -0
  185. quickthumb-0.3.1/tests/snapshots/image_with_text_overlay.png +0 -0
  186. quickthumb-0.3.1/tests/snapshots/jpeg_format.jpg +0 -0
  187. quickthumb-0.3.1/tests/snapshots/multiline_text_with_background.png +0 -0
  188. quickthumb-0.3.1/tests/snapshots/multiple_images.png +0 -0
  189. quickthumb-0.3.1/tests/snapshots/percentage_with_alignment.png +0 -0
  190. quickthumb-0.3.1/tests/snapshots/rich_text_advanced_styles.png +0 -0
  191. quickthumb-0.3.1/tests/snapshots/rich_text_alignment.png +0 -0
  192. quickthumb-0.3.1/tests/snapshots/rich_text_different_colors.png +0 -0
  193. quickthumb-0.3.1/tests/snapshots/rich_text_mixed_fonts.png +0 -0
  194. quickthumb-0.3.1/tests/snapshots/rich_text_mixed_styles.png +0 -0
  195. quickthumb-0.3.1/tests/snapshots/rich_text_with_background.png +0 -0
  196. quickthumb-0.3.1/tests/snapshots/rich_text_with_effects.png +0 -0
  197. quickthumb-0.3.1/tests/snapshots/roboto_bold.png +0 -0
  198. quickthumb-0.3.1/tests/snapshots/roboto_bold_italic.png +0 -0
  199. quickthumb-0.3.1/tests/snapshots/roboto_italic.png +0 -0
  200. quickthumb-0.3.1/tests/snapshots/shape_ellipse_basic.png +0 -0
  201. quickthumb-0.3.1/tests/snapshots/shape_rectangle_with_border_radius.png +0 -0
  202. quickthumb-0.3.1/tests/snapshots/shape_with_effects.png +0 -0
  203. quickthumb-0.3.1/tests/snapshots/shape_with_rotation.png +0 -0
  204. quickthumb-0.3.1/tests/snapshots/text_alignment.png +0 -0
  205. quickthumb-0.3.1/tests/snapshots/text_bold_and_italic.png +0 -0
  206. quickthumb-0.3.1/tests/snapshots/text_no_wrapping.png +0 -0
  207. quickthumb-0.3.1/tests/snapshots/text_rendering.png +0 -0
  208. quickthumb-0.3.1/tests/snapshots/text_rotation_basic.png +0 -0
  209. quickthumb-0.3.1/tests/snapshots/text_rotation_centered.png +0 -0
  210. quickthumb-0.3.1/tests/snapshots/text_rotation_with_effects.png +0 -0
  211. quickthumb-0.3.1/tests/snapshots/text_weight_black.png +0 -0
  212. quickthumb-0.3.1/tests/snapshots/text_weight_light.png +0 -0
  213. quickthumb-0.3.1/tests/snapshots/text_with_background_and_effects.png +0 -0
  214. quickthumb-0.3.1/tests/snapshots/text_with_background_basic.png +0 -0
  215. quickthumb-0.3.1/tests/snapshots/text_with_basic_shadow.png +0 -0
  216. quickthumb-0.3.1/tests/snapshots/text_with_blurred_shadow.png +0 -0
  217. quickthumb-0.3.1/tests/snapshots/text_with_glow.png +0 -0
  218. quickthumb-0.3.1/tests/snapshots/text_with_line_height.png +0 -0
  219. quickthumb-0.3.1/tests/snapshots/text_with_multiple_glows.png +0 -0
  220. quickthumb-0.3.1/tests/snapshots/text_with_stroke.png +0 -0
  221. quickthumb-0.3.1/tests/snapshots/text_wrapping_center_aligned.png +0 -0
  222. quickthumb-0.3.1/tests/snapshots/text_wrapping_center_aligned_percentage.png +0 -0
  223. quickthumb-0.3.1/tests/snapshots/text_wrapping_left_aligned.png +0 -0
  224. quickthumb-0.3.1/tests/snapshots/text_wrapping_right_aligned.png +0 -0
  225. quickthumb-0.3.1/tests/snapshots/webfont_basic_rendering.png +0 -0
  226. quickthumb-0.3.1/tests/snapshots/webp_format.webp +0 -0
  227. quickthumb-0.3.1/tests/test_canvas.py +0 -305
  228. {quickthumb-0.3.1 → quickthumb-0.5.0}/.claude/settings.json +0 -0
  229. {quickthumb-0.3.1 → quickthumb-0.5.0}/.gitignore +0 -0
  230. {quickthumb-0.3.1 → quickthumb-0.5.0}/.vscode/extensions.json +0 -0
  231. {quickthumb-0.3.1 → quickthumb-0.5.0}/.vscode/settings.json +0 -0
  232. {quickthumb-0.3.1 → quickthumb-0.5.0}/LICENSE +0 -0
  233. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-Black.ttf +0 -0
  234. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-BlackItalic.ttf +0 -0
  235. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-Bold.ttf +0 -0
  236. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-BoldItalic.ttf +0 -0
  237. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-ExtraBold.ttf +0 -0
  238. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-ExtraBoldItalic.ttf +0 -0
  239. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-ExtraLight.ttf +0 -0
  240. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-ExtraLightItalic.ttf +0 -0
  241. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-Italic.ttf +0 -0
  242. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-Light.ttf +0 -0
  243. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-LightItalic.ttf +0 -0
  244. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-Medium.ttf +0 -0
  245. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-MediumItalic.ttf +0 -0
  246. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-Regular.ttf +0 -0
  247. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-SemiBold.ttf +0 -0
  248. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-SemiBoldItalic.ttf +0 -0
  249. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-Thin.ttf +0 -0
  250. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSans-ThinItalic.ttf +0 -0
  251. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-Black.ttf +0 -0
  252. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-BlackItalic.ttf +0 -0
  253. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-Bold.ttf +0 -0
  254. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-BoldItalic.ttf +0 -0
  255. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-ExtraBold.ttf +0 -0
  256. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-ExtraBoldItalic.ttf +0 -0
  257. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-ExtraLight.ttf +0 -0
  258. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-ExtraLightItalic.ttf +0 -0
  259. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-Italic.ttf +0 -0
  260. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-Light.ttf +0 -0
  261. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-LightItalic.ttf +0 -0
  262. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-Medium.ttf +0 -0
  263. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-MediumItalic.ttf +0 -0
  264. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-Regular.ttf +0 -0
  265. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-SemiBold.ttf +0 -0
  266. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-SemiBoldItalic.ttf +0 -0
  267. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-Thin.ttf +0 -0
  268. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/NotoSerif-ThinItalic.ttf +0 -0
  269. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-Black.ttf +0 -0
  270. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-BlackItalic.ttf +0 -0
  271. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-Bold.ttf +0 -0
  272. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-BoldItalic.ttf +0 -0
  273. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-ExtraBold.ttf +0 -0
  274. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-ExtraBoldItalic.ttf +0 -0
  275. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-ExtraLight.ttf +0 -0
  276. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-ExtraLightItalic.ttf +0 -0
  277. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-Italic.ttf +0 -0
  278. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-Light.ttf +0 -0
  279. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-LightItalic.ttf +0 -0
  280. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-Medium.ttf +0 -0
  281. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-MediumItalic.ttf +0 -0
  282. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-Regular.ttf +0 -0
  283. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-SemiBold.ttf +0 -0
  284. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-SemiBoldItalic.ttf +0 -0
  285. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-Thin.ttf +0 -0
  286. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/fonts/Roboto-ThinItalic.ttf +0 -0
  287. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/images/c-g-JgDUVGAXsso-unsplash.jpg +0 -0
  288. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/images/denise-jans-WIRvXd1PYlg-unsplash.jpg +0 -0
  289. {quickthumb-0.3.1 → quickthumb-0.5.0}/assets/images/tobias-rademacher-wnF27F85ZKw-unsplash.jpg +0 -0
  290. {quickthumb-0.3.1 → quickthumb-0.5.0}/docs/assets/examples/youtube_thumbnail_01.png +0 -0
  291. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/instagram_news_card.py +0 -0
  292. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/podcast_interview_promo.py +0 -0
  293. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/youtube_reaction.py +0 -0
  294. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/youtube_talking_head.py +0 -0
  295. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/youtube_thumbnail_01.png +0 -0
  296. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/youtube_thumbnail_01.py +0 -0
  297. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/youtube_thumbnail_02.py +0 -0
  298. {quickthumb-0.3.1 → quickthumb-0.5.0}/examples/youtube_tutorial_explainer.py +0 -0
  299. {quickthumb-0.3.1 → quickthumb-0.5.0}/quickthumb/errors.py +0 -0
  300. {quickthumb-0.3.1 → quickthumb-0.5.0}/quickthumb/font_cache.py +0 -0
  301. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/__init__.py +0 -0
  302. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/conftest.py +0 -0
  303. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/fixtures/ivana-cajina-_7LbC5J-jw4-unsplash.jpg +0 -0
  304. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/fixtures/sample_image.jpg +0 -0
  305. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/fixtures/tobias-rademacher-wnF27F85ZKw-unsplash.jpg +0 -0
  306. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/arial_bold.png +0 -0
  307. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/arial_bold_italic.png +0 -0
  308. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/arial_italic.png +0 -0
  309. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/background_blur_heavy.png +0 -0
  310. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/background_blur_soft.png +0 -0
  311. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/background_combined_filters.png +0 -0
  312. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/background_contrast_high.png +0 -0
  313. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/background_contrast_low.png +0 -0
  314. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/background_filter_effect.png +0 -0
  315. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/background_saturation_grayscale.png +0 -0
  316. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/background_saturation_vivid.png +0 -0
  317. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/blend_mode_darken.png +0 -0
  318. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/blend_mode_lighten.png +0 -0
  319. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/blend_mode_multiply.png +0 -0
  320. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/blend_mode_normal.png +0 -0
  321. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/blend_mode_overlay.png +0 -0
  322. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/blend_mode_screen.png +0 -0
  323. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/bold_italic_with_named_font.png +0 -0
  324. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/bold_with_named_font.png +0 -0
  325. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/font_name_works.png +0 -0
  326. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_background_basic.png +0 -0
  327. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_background_with_opacity.png +0 -0
  328. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_basic.png +0 -0
  329. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_blend_mode_darken.png +0 -0
  330. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_blend_mode_lighten.png +0 -0
  331. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_blend_mode_multiply.png +0 -0
  332. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_blend_mode_normal.png +0 -0
  333. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_blend_mode_overlay.png +0 -0
  334. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_blend_mode_screen.png +0 -0
  335. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_brightness_decrease.png +0 -0
  336. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_brightness_increase.png +0 -0
  337. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_filter_effect.png +0 -0
  338. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_fit_contain.png +0 -0
  339. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_fit_cover.png +0 -0
  340. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_fit_fill.png +0 -0
  341. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_on_background_image.png +0 -0
  342. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_percentage_position.png +0 -0
  343. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_with_drop_shadow.png +0 -0
  344. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_with_glow.png +0 -0
  345. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_with_opacity.png +0 -0
  346. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_with_remove_background.png +0 -0
  347. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/image_with_stroke.png +0 -0
  348. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/italic_with_named_font.png +0 -0
  349. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/linear_gradient_brightness_decrease.png +0 -0
  350. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/linear_gradient_brightness_increase.png +0 -0
  351. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/linear_gradient_diagonal.png +0 -0
  352. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/linear_gradient_horizontal.png +0 -0
  353. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/linear_gradient_with_alpha.png +0 -0
  354. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/linear_gradient_with_opacity.png +0 -0
  355. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/multiline_text_with_glow.png +0 -0
  356. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/multiline_text_with_shadow.png +0 -0
  357. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/outline_basic.png +0 -0
  358. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/outline_with_offset.png +0 -0
  359. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/radial_gradient_brightness_decrease.png +0 -0
  360. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/radial_gradient_brightness_increase.png +0 -0
  361. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/radial_gradient_centered.png +0 -0
  362. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/radial_gradient_with_opacity.png +0 -0
  363. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/shape_rectangle_basic.png +0 -0
  364. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/shape_with_opacity.png +0 -0
  365. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/solid_background.png +0 -0
  366. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/solid_color_brightness_decrease.png +0 -0
  367. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/solid_color_brightness_increase.png +0 -0
  368. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/text_and_outline_with_opacity.png +0 -0
  369. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/text_with_letter_spacing.png +0 -0
  370. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/text_with_letter_spacing_glow.png +0 -0
  371. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/text_with_letter_spacing_shadow.png +0 -0
  372. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/times_new_roman_bold.png +0 -0
  373. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/times_new_roman_bold_italic.png +0 -0
  374. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/times_new_roman_italic.png +0 -0
  375. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/snapshots/url_image_background_basic.png +0 -0
  376. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/test_font_cache.py +0 -0
  377. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/test_image_layers.py +0 -0
  378. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/test_outline_layers.py +0 -0
  379. {quickthumb-0.3.1 → quickthumb-0.5.0}/tests/test_shape_layers.py +0 -0
@@ -0,0 +1,15 @@
1
+ ---
2
+ name: next-task
3
+ description: Read TASKS.md and begin working on the next TODO task in order.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ Read @specs/TASKS.md and begin working on the next [TODO] task.
8
+
9
+ - TODO → DOING when starting
10
+ - Continue to the next task if the total is < 200 lines. Set to [REVIEW] once finished.
11
+ - If code changes exceed ~200 lines: stop and set status to [REVIEW]
12
+ - Wait for user confirmation
13
+ - After confirmation: set status to [DONE] **before** committing or pushing
14
+
15
+ Do not reorder tasks. Only update the status.
@@ -1,7 +1,6 @@
1
1
  ---
2
2
  name: tdd
3
- description: Implement features or fix bugs using the TDD cycle. Use when you're about to add a new feature or fix a bug—start here before writing implementation code.
4
- disable-model-invocation: true
3
+ description: Follow the TDD cycle. Use this before writing any implementation code, whether fixing a bug, adding a feature, or making any code change.
5
4
  ---
6
5
 
7
6
  ## Workflow
@@ -4,6 +4,10 @@ on:
4
4
  push:
5
5
  tags:
6
6
  - "v*.*.*"
7
+ branches: [main]
8
+ paths:
9
+ - "docs/**"
10
+ - "mkdocs.yml"
7
11
  workflow_dispatch:
8
12
 
9
13
  permissions:
@@ -23,7 +23,7 @@ jobs:
23
23
 
24
24
  - name: Test
25
25
  run: |
26
- uv run pytest tests/ --ignore=tests/test_rendering.py
26
+ uv run --extra cli pytest tests/ --ignore=tests/test_rendering.py
27
27
 
28
28
  - name: Determine Version Change
29
29
  id: version_check
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickthumb
3
- Version: 0.3.1
3
+ Version: 0.5.0
4
4
  Summary: Programmatic thumbnail and social image generation with layered Python and JSON APIs
5
5
  Project-URL: Homepage, https://github.com/sjquant/quickthumb
6
6
  Project-URL: Repository, https://github.com/sjquant/quickthumb
@@ -24,14 +24,17 @@ Requires-Python: >=3.10
24
24
  Requires-Dist: pillow<11.0.0,>=10.4.0
25
25
  Requires-Dist: pydantic<3.0.0,>=2.12.5
26
26
  Requires-Dist: typing-extensions<5.0.0,>=4.15.0
27
+ Provides-Extra: cli
28
+ Requires-Dist: typer<1.0,>=0.24.1; extra == 'cli'
29
+ Requires-Dist: watchfiles<2.0.0,>=1.0.0; extra == 'cli'
27
30
  Provides-Extra: rembg
28
31
  Requires-Dist: onnxruntime<2.0.0,>=1.24.1; extra == 'rembg'
29
32
  Requires-Dist: rembg<3.0.0,>=2.0.50; extra == 'rembg'
30
33
  Description-Content-Type: text/markdown
31
34
 
32
- # QuickThumb
35
+ # quickthumb
33
36
 
34
- QuickThumb is a Python library for programmatic thumbnail, social card, and promo image generation.
37
+ quickthumb is a Python library for programmatic thumbnail, social card, and promo image generation.
35
38
  It is designed for code-first and JSON-first workflows, with a layer-based API that works well for human-authored scripts and AI-generated specs.
36
39
 
37
40
  ## Gallery
@@ -44,7 +47,7 @@ It is designed for code-first and JSON-first workflows, with a layer-based API t
44
47
  | --- | --- | --- |
45
48
  | ![Talking head thumbnail example](examples/youtube_talking_head.png) | ![Reaction thumbnail example](examples/youtube_reaction.png) | ![Tutorial thumbnail example](examples/youtube_tutorial_explainer.png) |
46
49
 
47
- ## Why QuickThumb
50
+ ## Why quickthumb
48
51
 
49
52
  - Built for thumbnails and social graphics, not just generic image composition
50
53
  - Works with Python method chaining and JSON serialization/deserialization
@@ -156,6 +159,56 @@ canvas = Canvas(1280, 720).text(
156
159
  )
157
160
  ```
158
161
 
162
+ ### Gradient and image-filled text
163
+
164
+ Fill text with a gradient or image instead of a flat color. Works on the whole text layer or per `TextPart`.
165
+
166
+ ```python
167
+ from quickthumb import Canvas, LinearGradient, RadialGradient, TextFillImage, TextPart
168
+
169
+ # Gradient headline
170
+ canvas = Canvas(1280, 720).text(
171
+ content="GRADIENT TITLE",
172
+ size=120,
173
+ fill=LinearGradient(
174
+ angle=90,
175
+ stops=[("#FF6B6B", 0.0), ("#FFE66D", 0.5), ("#4ECDC4", 1.0)],
176
+ ),
177
+ position=("50%", "50%"),
178
+ align="center",
179
+ )
180
+
181
+ # Image-filled text
182
+ canvas = Canvas(1280, 720).text(
183
+ content="TEXTURE",
184
+ size=140,
185
+ fill=TextFillImage(path="fire_texture.jpg", fit="cover"),
186
+ position=("50%", "50%"),
187
+ align="center",
188
+ )
189
+
190
+ # Per-segment fills using TextPart
191
+ canvas = Canvas(1280, 720).text(
192
+ content=[
193
+ TextPart(
194
+ text="HOT ",
195
+ fill=LinearGradient(angle=45, stops=[("#FF4500", 0.0), ("#FFD700", 1.0)]),
196
+ weight=900,
197
+ ),
198
+ TextPart(
199
+ text="COLD",
200
+ fill=LinearGradient(angle=45, stops=[("#00BFFF", 0.0), ("#8A2BE2", 1.0)]),
201
+ weight=900,
202
+ ),
203
+ ],
204
+ size=110,
205
+ position=("50%", "50%"),
206
+ align="center",
207
+ )
208
+ ```
209
+
210
+ `fill` accepts `LinearGradient`, `RadialGradient`, or `TextFillImage`. It is mutually independent of `color` — when `fill` is set it takes visual precedence. A `fill` on a `TextPart` overrides the layer-level `fill` for that segment only.
211
+
159
212
  ### Image layers
160
213
 
161
214
  ```python
@@ -194,6 +247,42 @@ canvas = Canvas(1280, 720).shape(
194
247
  )
195
248
  ```
196
249
 
250
+ ### Grain / noise effect
251
+
252
+ Add film-grain noise to background or image layers via `effects=[Grain(...)]`.
253
+
254
+ ```python
255
+ from quickthumb import Canvas, Grain
256
+
257
+ canvas = (
258
+ Canvas(1280, 720)
259
+ .background(
260
+ color="#1A1A2E",
261
+ effects=[Grain(intensity=0.12, monochrome=True)],
262
+ )
263
+ .image(
264
+ path="portrait.png",
265
+ position=("70%", "50%"),
266
+ width=400,
267
+ height=500,
268
+ align=("center", "middle"),
269
+ effects=[Grain(intensity=0.08, monochrome=False, blend_mode="overlay", opacity=0.6)],
270
+ )
271
+ )
272
+ ```
273
+
274
+ `Grain` parameters:
275
+
276
+ | Parameter | Type | Default | Description |
277
+ | --- | --- | --- | --- |
278
+ | `intensity` | `float` | required | Noise amplitude, `0.0`–`1.0` |
279
+ | `monochrome` | `bool` | `True` | `True` = luminance noise; `False` = per-channel color noise |
280
+ | `blend_mode` | `str` | `"overlay"` | `"overlay"`, `"screen"`, `"multiply"`, or `"normal"` |
281
+ | `opacity` | `float` | `1.0` | Overall grain strength, `0.0`–`1.0` |
282
+ | `seed` | `int \| None` | `None` | Optional RNG seed for deterministic output |
283
+
284
+ `Grain` is valid in `effects` on **background** and **image** layers. It is serialized with `"type": "grain"` in JSON.
285
+
197
286
  ### Export helpers
198
287
 
199
288
  ```python
@@ -204,7 +293,7 @@ canvas.render("output.webp", format="WEBP", quality=90)
204
293
 
205
294
  ## JSON-First Workflow
206
295
 
207
- QuickThumb can round-trip most canvases through JSON:
296
+ quickthumb can round-trip most canvases through JSON:
208
297
 
209
298
  ```python
210
299
  from quickthumb import Canvas
@@ -220,7 +309,7 @@ config = """
220
309
  },
221
310
  {
222
311
  "type": "text",
223
- "content": "Hello QuickThumb",
312
+ "content": "Hello quickthumb",
224
313
  "size": 72,
225
314
  "color": "#FFFFFF",
226
315
  "align": "center",
@@ -244,17 +333,17 @@ serialized = canvas.to_json()
244
333
  Notes:
245
334
 
246
335
  - JSON uses top-level `width`, `height`, and `layers`
247
- - Custom layers added with `canvas.custom(fn)` are not JSON-serializable
336
+ - Named custom layers added with `canvas.custom(fn, name="...", kwargs={...})` are JSON-serializable via the registry; unnamed custom layers are not
248
337
  - Enum-like values such as `blend_mode`, `fit`, and `align` can be passed as strings
249
338
 
250
339
  ## AI-Friendly Workflows
251
340
 
252
- QuickThumb is a good target when you want an LLM to generate image specs that are deterministic and easy to validate.
341
+ quickthumb is a good target when you want an LLM to generate image specs that are deterministic and easy to validate.
253
342
 
254
343
  Prompt pattern for Python generation:
255
344
 
256
345
  ```text
257
- Generate QuickThumb Python code for a 1280x720 YouTube thumbnail.
346
+ Generate quickthumb Python code for a 1280x720 YouTube thumbnail.
258
347
  Use layered composition only.
259
348
  Keep text on the left, subject image on the right, and use high-contrast typography.
260
349
  Return runnable code that ends with canvas.render("thumbnail.png").
@@ -263,20 +352,20 @@ Return runnable code that ends with canvas.render("thumbnail.png").
263
352
  Prompt pattern for JSON generation:
264
353
 
265
354
  ```text
266
- Generate a QuickThumb JSON config with top-level width, height, and layers.
355
+ Generate a quickthumb JSON config with top-level width, height, and layers.
267
356
  Use one background image layer, one dark overlay background layer, two text layers, and one outline layer.
268
- Only use valid QuickThumb layer types and effect names.
357
+ Only use valid quickthumb layer types and effect names.
269
358
  ```
270
359
 
271
360
  Recommended workflow:
272
361
 
273
- 1. Have the model produce QuickThumb Python or JSON.
362
+ 1. Have the model produce quickthumb Python or JSON.
274
363
  2. Validate or render it locally.
275
364
  3. Adjust only the content, colors, and assets instead of rewriting layout logic from scratch.
276
365
 
277
366
  ## Environment Variables
278
367
 
279
- QuickThumb looks for fonts using these environment variables:
368
+ quickthumb looks for fonts using these environment variables:
280
369
 
281
370
  - `QUICKTHUMB_FONT_DIR`: directory that contains font files
282
371
  - `QUICKTHUMB_DEFAULT_FONT`: default font family/name to use when `font` is omitted
@@ -299,13 +388,15 @@ os.environ["QUICKTHUMB_DEFAULT_FONT"] = "Roboto"
299
388
  | Background controls | Opacity, blend modes, fit modes, blur, brightness, contrast, saturation |
300
389
  | Text | Positioning, alignment, wrapping, letter spacing, line height, rotation, auto-scale |
301
390
  | Rich text | Per-segment `TextPart` styling |
391
+ | Text fills | Gradient (`LinearGradient`, `RadialGradient`) and image (`TextFillImage`) fills; per-`TextPart` override |
302
392
  | Text effects | Stroke, shadow, glow, background fill |
303
393
  | Fonts | Local fonts, CSS-style weights, italic/bold flags, webfont URLs, fallback mapping |
304
394
  | Images | Local/remote images, sizing, fit modes, alignment, opacity, rotation |
305
395
  | Image effects | Stroke, shadow, glow, filter effects, border radius, background removal |
396
+ | Grain / noise | Per-layer `Grain` effect on background and image layers; monochrome or color noise |
306
397
  | Shapes | Rectangle and ellipse primitives with stroke/shadow/glow support |
307
398
  | Export | PNG, JPEG, WebP, file output, base64, data URLs |
308
- | Serialization | `to_json()` / `from_json()` for built-in layer types |
399
+ | Serialization | `to_json()` / `from_json()` for built-in layer types and named custom layers |
309
400
 
310
401
  ## Real Example Scripts
311
402
 
@@ -320,7 +411,10 @@ See the shipped examples in [`examples/README.md`](examples/README.md):
320
411
  - `weight` and `bold=True` are mutually exclusive on text layers and `TextPart`
321
412
  - `auto_scale=True` requires `max_width`
322
413
  - `position` percentage values must be strings like `"50%"`
323
- - `canvas.custom(fn)` runs during render order and cannot be serialized to JSON
414
+ - `fill` and `color` are independent fields; when `fill` is set it takes visual precedence over `color`
415
+ - `canvas.custom(fn)` without a `name` runs during render order but cannot be serialized to JSON; pass `name=` and register the function with `Canvas.register_layer_fn()` to enable serialization
416
+ - `Grain` is valid only on background and image layer `effects`; it is not a valid text or shape effect
417
+ - `Grain(intensity=0.0)` is a no-op (no noise is generated or composited)
324
418
 
325
419
  ## Development
326
420
 
@@ -333,5 +427,4 @@ uv run ty quickthumb/
333
427
 
334
428
  ## Reference
335
429
 
336
- - Design notes: [DESIGN.md](DESIGN.md)
337
430
  - License: [LICENSE](LICENSE)
@@ -1,6 +1,6 @@
1
- # QuickThumb
1
+ # quickthumb
2
2
 
3
- QuickThumb is a Python library for programmatic thumbnail, social card, and promo image generation.
3
+ quickthumb is a Python library for programmatic thumbnail, social card, and promo image generation.
4
4
  It is designed for code-first and JSON-first workflows, with a layer-based API that works well for human-authored scripts and AI-generated specs.
5
5
 
6
6
  ## Gallery
@@ -13,7 +13,7 @@ It is designed for code-first and JSON-first workflows, with a layer-based API t
13
13
  | --- | --- | --- |
14
14
  | ![Talking head thumbnail example](examples/youtube_talking_head.png) | ![Reaction thumbnail example](examples/youtube_reaction.png) | ![Tutorial thumbnail example](examples/youtube_tutorial_explainer.png) |
15
15
 
16
- ## Why QuickThumb
16
+ ## Why quickthumb
17
17
 
18
18
  - Built for thumbnails and social graphics, not just generic image composition
19
19
  - Works with Python method chaining and JSON serialization/deserialization
@@ -125,6 +125,56 @@ canvas = Canvas(1280, 720).text(
125
125
  )
126
126
  ```
127
127
 
128
+ ### Gradient and image-filled text
129
+
130
+ Fill text with a gradient or image instead of a flat color. Works on the whole text layer or per `TextPart`.
131
+
132
+ ```python
133
+ from quickthumb import Canvas, LinearGradient, RadialGradient, TextFillImage, TextPart
134
+
135
+ # Gradient headline
136
+ canvas = Canvas(1280, 720).text(
137
+ content="GRADIENT TITLE",
138
+ size=120,
139
+ fill=LinearGradient(
140
+ angle=90,
141
+ stops=[("#FF6B6B", 0.0), ("#FFE66D", 0.5), ("#4ECDC4", 1.0)],
142
+ ),
143
+ position=("50%", "50%"),
144
+ align="center",
145
+ )
146
+
147
+ # Image-filled text
148
+ canvas = Canvas(1280, 720).text(
149
+ content="TEXTURE",
150
+ size=140,
151
+ fill=TextFillImage(path="fire_texture.jpg", fit="cover"),
152
+ position=("50%", "50%"),
153
+ align="center",
154
+ )
155
+
156
+ # Per-segment fills using TextPart
157
+ canvas = Canvas(1280, 720).text(
158
+ content=[
159
+ TextPart(
160
+ text="HOT ",
161
+ fill=LinearGradient(angle=45, stops=[("#FF4500", 0.0), ("#FFD700", 1.0)]),
162
+ weight=900,
163
+ ),
164
+ TextPart(
165
+ text="COLD",
166
+ fill=LinearGradient(angle=45, stops=[("#00BFFF", 0.0), ("#8A2BE2", 1.0)]),
167
+ weight=900,
168
+ ),
169
+ ],
170
+ size=110,
171
+ position=("50%", "50%"),
172
+ align="center",
173
+ )
174
+ ```
175
+
176
+ `fill` accepts `LinearGradient`, `RadialGradient`, or `TextFillImage`. It is mutually independent of `color` — when `fill` is set it takes visual precedence. A `fill` on a `TextPart` overrides the layer-level `fill` for that segment only.
177
+
128
178
  ### Image layers
129
179
 
130
180
  ```python
@@ -163,6 +213,42 @@ canvas = Canvas(1280, 720).shape(
163
213
  )
164
214
  ```
165
215
 
216
+ ### Grain / noise effect
217
+
218
+ Add film-grain noise to background or image layers via `effects=[Grain(...)]`.
219
+
220
+ ```python
221
+ from quickthumb import Canvas, Grain
222
+
223
+ canvas = (
224
+ Canvas(1280, 720)
225
+ .background(
226
+ color="#1A1A2E",
227
+ effects=[Grain(intensity=0.12, monochrome=True)],
228
+ )
229
+ .image(
230
+ path="portrait.png",
231
+ position=("70%", "50%"),
232
+ width=400,
233
+ height=500,
234
+ align=("center", "middle"),
235
+ effects=[Grain(intensity=0.08, monochrome=False, blend_mode="overlay", opacity=0.6)],
236
+ )
237
+ )
238
+ ```
239
+
240
+ `Grain` parameters:
241
+
242
+ | Parameter | Type | Default | Description |
243
+ | --- | --- | --- | --- |
244
+ | `intensity` | `float` | required | Noise amplitude, `0.0`–`1.0` |
245
+ | `monochrome` | `bool` | `True` | `True` = luminance noise; `False` = per-channel color noise |
246
+ | `blend_mode` | `str` | `"overlay"` | `"overlay"`, `"screen"`, `"multiply"`, or `"normal"` |
247
+ | `opacity` | `float` | `1.0` | Overall grain strength, `0.0`–`1.0` |
248
+ | `seed` | `int \| None` | `None` | Optional RNG seed for deterministic output |
249
+
250
+ `Grain` is valid in `effects` on **background** and **image** layers. It is serialized with `"type": "grain"` in JSON.
251
+
166
252
  ### Export helpers
167
253
 
168
254
  ```python
@@ -173,7 +259,7 @@ canvas.render("output.webp", format="WEBP", quality=90)
173
259
 
174
260
  ## JSON-First Workflow
175
261
 
176
- QuickThumb can round-trip most canvases through JSON:
262
+ quickthumb can round-trip most canvases through JSON:
177
263
 
178
264
  ```python
179
265
  from quickthumb import Canvas
@@ -189,7 +275,7 @@ config = """
189
275
  },
190
276
  {
191
277
  "type": "text",
192
- "content": "Hello QuickThumb",
278
+ "content": "Hello quickthumb",
193
279
  "size": 72,
194
280
  "color": "#FFFFFF",
195
281
  "align": "center",
@@ -213,17 +299,17 @@ serialized = canvas.to_json()
213
299
  Notes:
214
300
 
215
301
  - JSON uses top-level `width`, `height`, and `layers`
216
- - Custom layers added with `canvas.custom(fn)` are not JSON-serializable
302
+ - Named custom layers added with `canvas.custom(fn, name="...", kwargs={...})` are JSON-serializable via the registry; unnamed custom layers are not
217
303
  - Enum-like values such as `blend_mode`, `fit`, and `align` can be passed as strings
218
304
 
219
305
  ## AI-Friendly Workflows
220
306
 
221
- QuickThumb is a good target when you want an LLM to generate image specs that are deterministic and easy to validate.
307
+ quickthumb is a good target when you want an LLM to generate image specs that are deterministic and easy to validate.
222
308
 
223
309
  Prompt pattern for Python generation:
224
310
 
225
311
  ```text
226
- Generate QuickThumb Python code for a 1280x720 YouTube thumbnail.
312
+ Generate quickthumb Python code for a 1280x720 YouTube thumbnail.
227
313
  Use layered composition only.
228
314
  Keep text on the left, subject image on the right, and use high-contrast typography.
229
315
  Return runnable code that ends with canvas.render("thumbnail.png").
@@ -232,20 +318,20 @@ Return runnable code that ends with canvas.render("thumbnail.png").
232
318
  Prompt pattern for JSON generation:
233
319
 
234
320
  ```text
235
- Generate a QuickThumb JSON config with top-level width, height, and layers.
321
+ Generate a quickthumb JSON config with top-level width, height, and layers.
236
322
  Use one background image layer, one dark overlay background layer, two text layers, and one outline layer.
237
- Only use valid QuickThumb layer types and effect names.
323
+ Only use valid quickthumb layer types and effect names.
238
324
  ```
239
325
 
240
326
  Recommended workflow:
241
327
 
242
- 1. Have the model produce QuickThumb Python or JSON.
328
+ 1. Have the model produce quickthumb Python or JSON.
243
329
  2. Validate or render it locally.
244
330
  3. Adjust only the content, colors, and assets instead of rewriting layout logic from scratch.
245
331
 
246
332
  ## Environment Variables
247
333
 
248
- QuickThumb looks for fonts using these environment variables:
334
+ quickthumb looks for fonts using these environment variables:
249
335
 
250
336
  - `QUICKTHUMB_FONT_DIR`: directory that contains font files
251
337
  - `QUICKTHUMB_DEFAULT_FONT`: default font family/name to use when `font` is omitted
@@ -268,13 +354,15 @@ os.environ["QUICKTHUMB_DEFAULT_FONT"] = "Roboto"
268
354
  | Background controls | Opacity, blend modes, fit modes, blur, brightness, contrast, saturation |
269
355
  | Text | Positioning, alignment, wrapping, letter spacing, line height, rotation, auto-scale |
270
356
  | Rich text | Per-segment `TextPart` styling |
357
+ | Text fills | Gradient (`LinearGradient`, `RadialGradient`) and image (`TextFillImage`) fills; per-`TextPart` override |
271
358
  | Text effects | Stroke, shadow, glow, background fill |
272
359
  | Fonts | Local fonts, CSS-style weights, italic/bold flags, webfont URLs, fallback mapping |
273
360
  | Images | Local/remote images, sizing, fit modes, alignment, opacity, rotation |
274
361
  | Image effects | Stroke, shadow, glow, filter effects, border radius, background removal |
362
+ | Grain / noise | Per-layer `Grain` effect on background and image layers; monochrome or color noise |
275
363
  | Shapes | Rectangle and ellipse primitives with stroke/shadow/glow support |
276
364
  | Export | PNG, JPEG, WebP, file output, base64, data URLs |
277
- | Serialization | `to_json()` / `from_json()` for built-in layer types |
365
+ | Serialization | `to_json()` / `from_json()` for built-in layer types and named custom layers |
278
366
 
279
367
  ## Real Example Scripts
280
368
 
@@ -289,7 +377,10 @@ See the shipped examples in [`examples/README.md`](examples/README.md):
289
377
  - `weight` and `bold=True` are mutually exclusive on text layers and `TextPart`
290
378
  - `auto_scale=True` requires `max_width`
291
379
  - `position` percentage values must be strings like `"50%"`
292
- - `canvas.custom(fn)` runs during render order and cannot be serialized to JSON
380
+ - `fill` and `color` are independent fields; when `fill` is set it takes visual precedence over `color`
381
+ - `canvas.custom(fn)` without a `name` runs during render order but cannot be serialized to JSON; pass `name=` and register the function with `Canvas.register_layer_fn()` to enable serialization
382
+ - `Grain` is valid only on background and image layer `effects`; it is not a valid text or shape effect
383
+ - `Grain(intensity=0.0)` is a no-op (no noise is generated or composited)
293
384
 
294
385
  ## Development
295
386
 
@@ -302,5 +393,4 @@ uv run ty quickthumb/
302
393
 
303
394
  ## Reference
304
395
 
305
- - Design notes: [DESIGN.md](DESIGN.md)
306
396
  - License: [LICENSE](LICENSE)
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Reference for quickthumb background layers, including colors, gradients, images, opacity, effects, and render order.
3
+ ---
4
+
1
5
  # Background
2
6
 
3
7
  `.background()` adds a full-canvas background layer. Multiple background calls stack in render order — first call is backmost.
@@ -26,7 +30,7 @@ canvas.background(
26
30
  | `opacity` | `float` | `1.0` | Layer opacity from `0.0` (transparent) to `1.0` (opaque). |
27
31
  | `blend_mode` | `str \| BlendMode \| None` | `None` | How this layer composites over previous layers. See [BlendMode](enums.md#blendmode). |
28
32
  | `fit` | `str \| FitMode \| None` | `None` | How an image fills the canvas. See [FitMode](enums.md#fitmode). |
29
- | `effects` | `list[Filter] \| None` | `[]` | Background effects. Currently only `Filter` is supported. |
33
+ | `effects` | `list \| None` | `[]` | Background effects. Accepts `Filter` and `Grain`. |
30
34
 
31
35
  ## Examples
32
36
 
@@ -75,4 +79,4 @@ canvas.background(color="#000000", opacity=0.4)
75
79
  - `color`, `gradient`, and `image` can each be used independently or together within one layer.
76
80
  - `blend_mode` applies when compositing this layer over previous layers.
77
81
  - Supports both local file paths and remote URLs for `image`.
78
- - For available effects, see [Filter](effects.md#filter).
82
+ - For available effects, see [Filter](effects.md#filter) and [Grain](effects.md#grain).
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Reference for quickthumb Canvas creation, aspect ratios, layer composition, rendering, JSON loading, and image output.
3
+ ---
4
+
1
5
  # Canvas
2
6
 
3
7
  The `Canvas` is the root object. It holds dimensions and an ordered list of layers.
@@ -1,3 +1,7 @@
1
+ ---
2
+ description: Reference for quickthumb effects, including filters, shadows, strokes, blend modes, opacity, and layer compatibility.
3
+ ---
4
+
1
5
  # Effects
2
6
 
3
7
  Effects are modifiers applied to layers. Each layer type accepts a specific set.
@@ -9,6 +13,7 @@ Effects are modifiers applied to layers. Each layer type accepts a specific set.
9
13
  | `Glow` | ✓ | ✓ | ✓ | — |
10
14
  | `Filter` | — | ✓ | — | ✓ |
11
15
  | `Background` | ✓ | — | — | — |
16
+ | `Grain` | — | ✓ | — | ✓ |
12
17
 
13
18
  Pass effects as a list to the `effects` parameter of any layer:
14
19
 
@@ -193,3 +198,50 @@ canvas.text(
193
198
 
194
199
  !!! note
195
200
  `Background` as an effect is separate from the `.background()` layer builder. The effect applies behind a text block; the layer covers the full canvas.
201
+
202
+ ---
203
+
204
+ ## Grain
205
+
206
+ Adds film-grain noise to a background or image layer.
207
+
208
+ ```python
209
+ from quickthumb import Grain
210
+
211
+ Grain(intensity=0.12, monochrome=True, blend_mode="overlay", opacity=1.0)
212
+ ```
213
+
214
+ | Parameter | Type | Default | Description |
215
+ | --- | --- | --- | --- |
216
+ | `intensity` | `float` | **required** | Noise amplitude from `0.0` (none) to `1.0` (maximum). |
217
+ | `monochrome` | `bool` | `True` | `True` = luminance noise (grey grain); `False` = independent per-channel color noise. |
218
+ | `blend_mode` | `str` | `"overlay"` | How noise composites onto the layer. One of `"overlay"`, `"screen"`, `"multiply"`, `"normal"`. |
219
+ | `opacity` | `float` | `1.0` | Overall grain strength from `0.0` (invisible) to `1.0` (full). |
220
+ | `seed` | `int \| None` | `None` | RNG seed for deterministic output. `None` = random each render. |
221
+
222
+ `intensity=0.0` is a no-op — no noise is generated.
223
+
224
+ ### Example
225
+
226
+ ```python
227
+ from quickthumb import Canvas, Grain
228
+
229
+ canvas = (
230
+ Canvas(1280, 720)
231
+ .background(
232
+ color="#1A1A2E",
233
+ effects=[Grain(intensity=0.12, monochrome=True)],
234
+ )
235
+ .image(
236
+ path="portrait.png",
237
+ position=("70%", "50%"),
238
+ width=400,
239
+ height=500,
240
+ align=("center", "middle"),
241
+ effects=[Grain(intensity=0.08, monochrome=False, blend_mode="overlay", opacity=0.6)],
242
+ )
243
+ )
244
+ ```
245
+
246
+ !!! note
247
+ `Grain` is valid only on **background** and **image** layers. It is not available on text or shape layers.