quickthumb 0.3.0__tar.gz → 0.4.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 (277) hide show
  1. quickthumb-0.4.0/.github/workflows/docs.yml +30 -0
  2. {quickthumb-0.3.0 → quickthumb-0.4.0}/.gitignore +3 -0
  3. quickthumb-0.4.0/DESIGN.md +606 -0
  4. {quickthumb-0.3.0 → quickthumb-0.4.0}/PKG-INFO +8 -4
  5. {quickthumb-0.3.0 → quickthumb-0.4.0}/README.md +7 -3
  6. quickthumb-0.4.0/docs/api/background.md +78 -0
  7. quickthumb-0.4.0/docs/api/canvas.md +141 -0
  8. quickthumb-0.4.0/docs/api/effects.md +195 -0
  9. quickthumb-0.4.0/docs/api/enums.md +194 -0
  10. quickthumb-0.4.0/docs/api/image.md +131 -0
  11. quickthumb-0.4.0/docs/api/index.md +43 -0
  12. quickthumb-0.4.0/docs/api/outline.md +61 -0
  13. quickthumb-0.4.0/docs/api/shape.md +110 -0
  14. quickthumb-0.4.0/docs/api/text.md +194 -0
  15. quickthumb-0.4.0/docs/assets/examples/instagram_news_card.png +0 -0
  16. quickthumb-0.4.0/docs/assets/examples/podcast_interview_promo.png +0 -0
  17. quickthumb-0.4.0/docs/assets/examples/shorts_cover_agent.png +0 -0
  18. quickthumb-0.4.0/docs/assets/examples/youtube_reaction.png +0 -0
  19. quickthumb-0.4.0/docs/assets/examples/youtube_talking_head.png +0 -0
  20. {quickthumb-0.3.0 → quickthumb-0.4.0/docs/assets}/examples/youtube_thumbnail_02.png +0 -0
  21. quickthumb-0.4.0/docs/assets/examples/youtube_tutorial_explainer.png +0 -0
  22. quickthumb-0.4.0/docs/changelog.md +5 -0
  23. quickthumb-0.4.0/docs/concepts.md +255 -0
  24. quickthumb-0.4.0/docs/cookbook/ai-workflow.md +182 -0
  25. quickthumb-0.4.0/docs/cookbook/index.md +46 -0
  26. quickthumb-0.4.0/docs/cookbook/instagram-card.md +129 -0
  27. quickthumb-0.4.0/docs/cookbook/podcast-promo.md +214 -0
  28. quickthumb-0.4.0/docs/cookbook/shorts-cover.md +208 -0
  29. quickthumb-0.4.0/docs/cookbook/webfonts-rembg.md +199 -0
  30. quickthumb-0.4.0/docs/cookbook/youtube-thumbnail.md +348 -0
  31. quickthumb-0.4.0/docs/faq.md +170 -0
  32. quickthumb-0.4.0/docs/getting-started.md +206 -0
  33. quickthumb-0.4.0/docs/index.md +59 -0
  34. quickthumb-0.4.0/docs/installation.md +56 -0
  35. quickthumb-0.4.0/docs/json-schema.md +365 -0
  36. quickthumb-0.4.0/docs/stylesheets/extra.css +83 -0
  37. quickthumb-0.4.0/examples/README.md +155 -0
  38. quickthumb-0.4.0/examples/instagram_news_card.png +0 -0
  39. quickthumb-0.4.0/examples/podcast_interview_promo.png +0 -0
  40. quickthumb-0.4.0/examples/podcast_interview_promo.py +146 -0
  41. quickthumb-0.4.0/examples/shorts_cover_agent.json +302 -0
  42. quickthumb-0.4.0/examples/shorts_cover_agent.png +0 -0
  43. quickthumb-0.4.0/examples/shorts_cover_agent.py +33 -0
  44. quickthumb-0.4.0/examples/youtube_reaction.png +0 -0
  45. quickthumb-0.4.0/examples/youtube_reaction.py +103 -0
  46. quickthumb-0.4.0/examples/youtube_talking_head.png +0 -0
  47. quickthumb-0.4.0/examples/youtube_talking_head.py +102 -0
  48. quickthumb-0.4.0/examples/youtube_thumbnail_01.png +0 -0
  49. quickthumb-0.4.0/examples/youtube_thumbnail_02.png +0 -0
  50. quickthumb-0.4.0/examples/youtube_tutorial_explainer.png +0 -0
  51. quickthumb-0.4.0/examples/youtube_tutorial_explainer.py +136 -0
  52. quickthumb-0.4.0/mkdocs.yml +87 -0
  53. {quickthumb-0.3.0 → quickthumb-0.4.0}/pyproject.toml +4 -1
  54. {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/canvas.py +190 -87
  55. quickthumb-0.4.0/tests/snapshots/custom_layer_with_kwargs.png +0 -0
  56. quickthumb-0.4.0/tests/snapshots/explicit_newlines_preserved_with_max_width.png +0 -0
  57. quickthumb-0.4.0/tests/snapshots/image_with_border_radius.png +0 -0
  58. quickthumb-0.4.0/tests/snapshots/image_with_drop_shadow.png +0 -0
  59. quickthumb-0.4.0/tests/snapshots/image_with_rotation.png +0 -0
  60. quickthumb-0.4.0/tests/snapshots/multiline_text_with_background.png +0 -0
  61. quickthumb-0.4.0/tests/snapshots/multiple_images.png +0 -0
  62. quickthumb-0.4.0/tests/snapshots/rich_text_with_background.png +0 -0
  63. quickthumb-0.4.0/tests/snapshots/shape_ellipse_basic.png +0 -0
  64. quickthumb-0.4.0/tests/snapshots/shape_rectangle_with_border_radius.png +0 -0
  65. quickthumb-0.4.0/tests/snapshots/shape_with_effects.png +0 -0
  66. quickthumb-0.4.0/tests/snapshots/shape_with_rotation.png +0 -0
  67. quickthumb-0.4.0/tests/snapshots/text_with_background_and_effects.png +0 -0
  68. quickthumb-0.4.0/tests/snapshots/text_with_background_basic.png +0 -0
  69. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_canvas.py +113 -15
  70. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_rendering.py +103 -3
  71. {quickthumb-0.3.0 → quickthumb-0.4.0}/uv.lock +396 -1
  72. quickthumb-0.3.0/DESIGN.md +0 -476
  73. quickthumb-0.3.0/PROGRESS.md +0 -123
  74. quickthumb-0.3.0/examples/README.md +0 -78
  75. quickthumb-0.3.0/examples/instagram_news_card.png +0 -0
  76. quickthumb-0.3.0/tests/snapshots/image_with_border_radius.png +0 -0
  77. quickthumb-0.3.0/tests/snapshots/image_with_drop_shadow.png +0 -0
  78. quickthumb-0.3.0/tests/snapshots/image_with_rotation.png +0 -0
  79. quickthumb-0.3.0/tests/snapshots/multiple_images.png +0 -0
  80. quickthumb-0.3.0/tests/snapshots/rich_text_with_background.png +0 -0
  81. quickthumb-0.3.0/tests/snapshots/shape_ellipse_basic.png +0 -0
  82. quickthumb-0.3.0/tests/snapshots/shape_rectangle_with_border_radius.png +0 -0
  83. quickthumb-0.3.0/tests/snapshots/shape_with_effects.png +0 -0
  84. quickthumb-0.3.0/tests/snapshots/shape_with_rotation.png +0 -0
  85. quickthumb-0.3.0/tests/snapshots/text_with_background_and_effects.png +0 -0
  86. quickthumb-0.3.0/tests/snapshots/text_with_background_basic.png +0 -0
  87. {quickthumb-0.3.0 → quickthumb-0.4.0}/.claude/settings.json +0 -0
  88. {quickthumb-0.3.0 → quickthumb-0.4.0}/.claude/skills/tdd/SKILL.md +0 -0
  89. {quickthumb-0.3.0 → quickthumb-0.4.0}/.github/workflows/publish.yaml +0 -0
  90. {quickthumb-0.3.0 → quickthumb-0.4.0}/.vscode/extensions.json +0 -0
  91. {quickthumb-0.3.0 → quickthumb-0.4.0}/.vscode/settings.json +0 -0
  92. {quickthumb-0.3.0 → quickthumb-0.4.0}/CLAUDE.md +0 -0
  93. {quickthumb-0.3.0 → quickthumb-0.4.0}/LICENSE +0 -0
  94. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Black.ttf +0 -0
  95. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-BlackItalic.ttf +0 -0
  96. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Bold.ttf +0 -0
  97. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-BoldItalic.ttf +0 -0
  98. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ExtraBold.ttf +0 -0
  99. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ExtraBoldItalic.ttf +0 -0
  100. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ExtraLight.ttf +0 -0
  101. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ExtraLightItalic.ttf +0 -0
  102. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Italic.ttf +0 -0
  103. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Light.ttf +0 -0
  104. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-LightItalic.ttf +0 -0
  105. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Medium.ttf +0 -0
  106. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-MediumItalic.ttf +0 -0
  107. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Regular.ttf +0 -0
  108. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-SemiBold.ttf +0 -0
  109. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-SemiBoldItalic.ttf +0 -0
  110. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Thin.ttf +0 -0
  111. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ThinItalic.ttf +0 -0
  112. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Black.ttf +0 -0
  113. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-BlackItalic.ttf +0 -0
  114. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Bold.ttf +0 -0
  115. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-BoldItalic.ttf +0 -0
  116. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ExtraBold.ttf +0 -0
  117. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ExtraBoldItalic.ttf +0 -0
  118. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ExtraLight.ttf +0 -0
  119. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ExtraLightItalic.ttf +0 -0
  120. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Italic.ttf +0 -0
  121. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Light.ttf +0 -0
  122. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-LightItalic.ttf +0 -0
  123. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Medium.ttf +0 -0
  124. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-MediumItalic.ttf +0 -0
  125. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Regular.ttf +0 -0
  126. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-SemiBold.ttf +0 -0
  127. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-SemiBoldItalic.ttf +0 -0
  128. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Thin.ttf +0 -0
  129. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ThinItalic.ttf +0 -0
  130. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Black.ttf +0 -0
  131. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-BlackItalic.ttf +0 -0
  132. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Bold.ttf +0 -0
  133. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-BoldItalic.ttf +0 -0
  134. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ExtraBold.ttf +0 -0
  135. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ExtraBoldItalic.ttf +0 -0
  136. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ExtraLight.ttf +0 -0
  137. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ExtraLightItalic.ttf +0 -0
  138. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Italic.ttf +0 -0
  139. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Light.ttf +0 -0
  140. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-LightItalic.ttf +0 -0
  141. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Medium.ttf +0 -0
  142. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-MediumItalic.ttf +0 -0
  143. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Regular.ttf +0 -0
  144. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-SemiBold.ttf +0 -0
  145. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-SemiBoldItalic.ttf +0 -0
  146. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Thin.ttf +0 -0
  147. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ThinItalic.ttf +0 -0
  148. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/images/c-g-JgDUVGAXsso-unsplash.jpg +0 -0
  149. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/images/denise-jans-WIRvXd1PYlg-unsplash.jpg +0 -0
  150. {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/images/tobias-rademacher-wnF27F85ZKw-unsplash.jpg +0 -0
  151. {quickthumb-0.3.0 → quickthumb-0.4.0/docs/assets}/examples/youtube_thumbnail_01.png +0 -0
  152. {quickthumb-0.3.0 → quickthumb-0.4.0}/examples/instagram_news_card.py +0 -0
  153. {quickthumb-0.3.0 → quickthumb-0.4.0}/examples/youtube_thumbnail_01.py +0 -0
  154. {quickthumb-0.3.0 → quickthumb-0.4.0}/examples/youtube_thumbnail_02.py +0 -0
  155. {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/__init__.py +0 -0
  156. {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/errors.py +0 -0
  157. {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/font_cache.py +0 -0
  158. {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/models.py +0 -0
  159. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/__init__.py +0 -0
  160. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/conftest.py +0 -0
  161. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/fixtures/ivana-cajina-_7LbC5J-jw4-unsplash.jpg +0 -0
  162. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/fixtures/sample_image.jpg +0 -0
  163. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/fixtures/tobias-rademacher-wnF27F85ZKw-unsplash.jpg +0 -0
  164. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/arial_bold.png +0 -0
  165. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/arial_bold_italic.png +0 -0
  166. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/arial_italic.png +0 -0
  167. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/auto_scale_rich_text.png +0 -0
  168. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/auto_scale_simple_text.png +0 -0
  169. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_blur_heavy.png +0 -0
  170. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_blur_soft.png +0 -0
  171. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_combined_filters.png +0 -0
  172. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_contrast_high.png +0 -0
  173. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_contrast_low.png +0 -0
  174. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_filter_effect.png +0 -0
  175. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_saturation_grayscale.png +0 -0
  176. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_saturation_vivid.png +0 -0
  177. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_darken.png +0 -0
  178. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_lighten.png +0 -0
  179. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_multiply.png +0 -0
  180. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_normal.png +0 -0
  181. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_overlay.png +0 -0
  182. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_screen.png +0 -0
  183. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/bold_italic_with_named_font.png +0 -0
  184. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/bold_with_named_font.png +0 -0
  185. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/custom_canvas_layer.png +0 -0
  186. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/font_name_works.png +0 -0
  187. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_background_basic.png +0 -0
  188. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_background_with_opacity.png +0 -0
  189. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_basic.png +0 -0
  190. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_darken.png +0 -0
  191. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_lighten.png +0 -0
  192. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_multiply.png +0 -0
  193. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_normal.png +0 -0
  194. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_overlay.png +0 -0
  195. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_screen.png +0 -0
  196. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_brightness_decrease.png +0 -0
  197. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_brightness_increase.png +0 -0
  198. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_filter_effect.png +0 -0
  199. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_fit_contain.png +0 -0
  200. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_fit_cover.png +0 -0
  201. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_fit_fill.png +0 -0
  202. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_on_background_image.png +0 -0
  203. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_overlay_fit_contain.png +0 -0
  204. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_overlay_fit_cover.png +0 -0
  205. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_overlay_fit_fill.png +0 -0
  206. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_percentage_position.png +0 -0
  207. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_glow.png +0 -0
  208. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_opacity.png +0 -0
  209. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_remove_background.png +0 -0
  210. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_stroke.png +0 -0
  211. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_text_overlay.png +0 -0
  212. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/italic_with_named_font.png +0 -0
  213. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/jpeg_format.jpg +0 -0
  214. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_brightness_decrease.png +0 -0
  215. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_brightness_increase.png +0 -0
  216. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_diagonal.png +0 -0
  217. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_horizontal.png +0 -0
  218. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_with_alpha.png +0 -0
  219. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_with_opacity.png +0 -0
  220. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/multiline_text_with_glow.png +0 -0
  221. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/multiline_text_with_shadow.png +0 -0
  222. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/outline_basic.png +0 -0
  223. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/outline_with_offset.png +0 -0
  224. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/percentage_with_alignment.png +0 -0
  225. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/radial_gradient_brightness_decrease.png +0 -0
  226. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/radial_gradient_brightness_increase.png +0 -0
  227. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/radial_gradient_centered.png +0 -0
  228. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/radial_gradient_with_opacity.png +0 -0
  229. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_advanced_styles.png +0 -0
  230. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_alignment.png +0 -0
  231. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_different_colors.png +0 -0
  232. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_mixed_fonts.png +0 -0
  233. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_mixed_styles.png +0 -0
  234. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_with_effects.png +0 -0
  235. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/roboto_bold.png +0 -0
  236. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/roboto_bold_italic.png +0 -0
  237. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/roboto_italic.png +0 -0
  238. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/shape_rectangle_basic.png +0 -0
  239. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/shape_with_opacity.png +0 -0
  240. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/solid_background.png +0 -0
  241. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/solid_color_brightness_decrease.png +0 -0
  242. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/solid_color_brightness_increase.png +0 -0
  243. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_alignment.png +0 -0
  244. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_and_outline_with_opacity.png +0 -0
  245. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_bold_and_italic.png +0 -0
  246. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_no_wrapping.png +0 -0
  247. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_rendering.png +0 -0
  248. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_rotation_basic.png +0 -0
  249. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_rotation_centered.png +0 -0
  250. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_rotation_with_effects.png +0 -0
  251. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_weight_black.png +0 -0
  252. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_weight_light.png +0 -0
  253. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_basic_shadow.png +0 -0
  254. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_blurred_shadow.png +0 -0
  255. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_glow.png +0 -0
  256. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_letter_spacing.png +0 -0
  257. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_letter_spacing_glow.png +0 -0
  258. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_letter_spacing_shadow.png +0 -0
  259. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_line_height.png +0 -0
  260. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_multiple_glows.png +0 -0
  261. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_stroke.png +0 -0
  262. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_wrapping_center_aligned.png +0 -0
  263. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_wrapping_center_aligned_percentage.png +0 -0
  264. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_wrapping_left_aligned.png +0 -0
  265. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_wrapping_right_aligned.png +0 -0
  266. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/times_new_roman_bold.png +0 -0
  267. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/times_new_roman_bold_italic.png +0 -0
  268. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/times_new_roman_italic.png +0 -0
  269. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/url_image_background_basic.png +0 -0
  270. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/webfont_basic_rendering.png +0 -0
  271. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/webp_format.webp +0 -0
  272. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_background_layers.py +0 -0
  273. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_font_cache.py +0 -0
  274. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_image_layers.py +0 -0
  275. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_outline_layers.py +0 -0
  276. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_shape_layers.py +0 -0
  277. {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_text_layers.py +0 -0
@@ -0,0 +1,30 @@
1
+ name: Deploy docs
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v*.*.*"
7
+ branches: [main]
8
+ paths:
9
+ - "docs/**"
10
+ - "mkdocs.yml"
11
+ workflow_dispatch:
12
+
13
+ permissions:
14
+ contents: write
15
+
16
+ jobs:
17
+ deploy:
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v4
21
+
22
+ - uses: astral-sh/setup-uv@v5
23
+ with:
24
+ enable-cache: true
25
+
26
+ - name: Install docs dependencies
27
+ run: uv sync --group docs
28
+
29
+ - name: Deploy to GitHub Pages
30
+ run: uv run --group docs mkdocs gh-deploy --force
@@ -1,6 +1,9 @@
1
1
  # AI agent directories
2
2
  .agents
3
3
 
4
+ # macOS Finder metadata
5
+ .DS_Store
6
+
4
7
  # Byte-compiled / optimized / DLL files
5
8
  __pycache__/
6
9
  *.py[cod]
@@ -0,0 +1,606 @@
1
+ # QuickThumb API Reference
2
+
3
+ This document reflects the current implemented API in this repository.
4
+ It is intended to be the reliable reference for writing QuickThumb code, generating QuickThumb JSON, and validating what the library actually supports today.
5
+
6
+ ## Core Model
7
+
8
+ - A `Canvas` is an ordered list of layers.
9
+ - Layers render in call order: first added is the backmost layer.
10
+ - All layer builder methods mutate the canvas and return `self`.
11
+ - Most canvases can round-trip through JSON with `to_json()` and `from_json()`.
12
+ - Named custom callback layers can round-trip through JSON via a registry. Unnamed custom layers cannot be serialized.
13
+
14
+ ## Public Imports
15
+
16
+ ```python
17
+ from quickthumb import (
18
+ Align,
19
+ Background,
20
+ BlendMode,
21
+ Canvas,
22
+ Filter,
23
+ FitMode,
24
+ Glow,
25
+ LinearGradient,
26
+ RadialGradient,
27
+ Shadow,
28
+ Stroke,
29
+ TextPart,
30
+ ValidationError,
31
+ )
32
+ ```
33
+
34
+ ## Canvas API
35
+
36
+ ### Creation
37
+
38
+ ```python
39
+ from quickthumb import Canvas
40
+
41
+ canvas = Canvas(1280, 720)
42
+ wide = Canvas.from_aspect_ratio("16:9", base_width=1280)
43
+ square = Canvas.from_aspect_ratio("1:1", base_width=1080)
44
+ vertical = Canvas.from_aspect_ratio("9:16", base_width=1080)
45
+ ```
46
+
47
+ Rules:
48
+
49
+ - `width` and `height` must be positive integers.
50
+ - `from_aspect_ratio()` expects a string like `"16:9"` plus `base_width`.
51
+
52
+ ### Layer Builders
53
+
54
+ ```python
55
+ canvas.background(...)
56
+ canvas.text(...)
57
+ canvas.outline(...)
58
+ canvas.shape(...)
59
+ canvas.image(...)
60
+ canvas.custom(fn)
61
+ canvas.custom(fn, name="...", kwargs={...})
62
+ Canvas.register_layer_fn(name, fn)
63
+ Canvas.unregister_layer_fn(name)
64
+ ```
65
+
66
+ ### Export Methods
67
+
68
+ ```python
69
+ canvas.render("output.png")
70
+ canvas.render("output.webp", format="WEBP", quality=90)
71
+
72
+ png_base64 = canvas.to_base64(format="PNG")
73
+ jpeg_data_url = canvas.to_data_url(format="JPEG", quality=90)
74
+
75
+ json_data = canvas.to_json()
76
+ canvas = Canvas.from_json(json_data)
77
+ ```
78
+
79
+ Rules:
80
+
81
+ - Supported output formats are `PNG`, `JPEG`, and `WEBP`.
82
+ - `quality` is only valid for `JPEG` and `WEBP`.
83
+ - `Canvas.from_json()` accepts a JSON string, not a Python dict.
84
+
85
+ ## Layer Reference
86
+
87
+ ### Background Layer
88
+
89
+ Adds a full-canvas background layer.
90
+
91
+ ```python
92
+ from quickthumb import Canvas, Filter, LinearGradient
93
+
94
+ canvas = (
95
+ Canvas(1280, 720)
96
+ .background(color="#0F172A")
97
+ .background(
98
+ gradient=LinearGradient(
99
+ angle=120,
100
+ stops=[("#111827", 0.0), ("#11182700", 1.0)],
101
+ ),
102
+ opacity=0.7,
103
+ )
104
+ .background(
105
+ image="https://images.unsplash.com/photo-1516321318423-f06f85e504b3",
106
+ fit="cover",
107
+ blend_mode="multiply",
108
+ effects=[Filter(blur=4, brightness=0.75, contrast=1.1, saturation=0.9)],
109
+ )
110
+ )
111
+ ```
112
+
113
+ Parameters:
114
+
115
+ - `color`: hex string (`"#RRGGBB"` or `"#RRGGBBAA"`) or RGB/RGBA tuple
116
+ - `gradient`: `LinearGradient` or `RadialGradient`
117
+ - `image`: local file path or remote URL
118
+ - `opacity`: float from `0.0` to `1.0`
119
+ - `blend_mode`: `multiply`, `overlay`, `screen`, `darken`, `lighten`, or `normal`
120
+ - `fit`: `cover`, `contain`, or `fill`
121
+ - `effects`: background effects list, currently `Filter` only
122
+
123
+ Notes:
124
+
125
+ - Background layers cover the full canvas.
126
+ - Background blend modes apply when compositing over previous layers.
127
+ - Backgrounds support local images and remote URLs.
128
+
129
+ ### Text Layer
130
+
131
+ Adds a text layer using either a plain string or rich text parts.
132
+
133
+ ```python
134
+ from quickthumb import Background, Canvas, Glow, Shadow, Stroke, TextPart
135
+
136
+ canvas = Canvas(1280, 720).text(
137
+ content=[
138
+ TextPart(text="BUILD ", color="#B8FF00", weight=900),
139
+ TextPart(text="FASTER", color="#FFFFFF", effects=[Stroke(width=4, color="#000000")]),
140
+ ],
141
+ font="Impact",
142
+ size=110,
143
+ color="#FFFFFF",
144
+ position=("8%", "50%"),
145
+ align=("left", "middle"),
146
+ max_width="60%",
147
+ line_height=1.0,
148
+ letter_spacing=1,
149
+ effects=[
150
+ Background(color="#111827CC", padding=(16, 24), border_radius=14),
151
+ Shadow(offset_x=4, offset_y=4, color="#000000", blur_radius=8),
152
+ Glow(color="#B8FF00", radius=16, opacity=0.25),
153
+ ],
154
+ rotation=0,
155
+ opacity=1.0,
156
+ )
157
+ ```
158
+
159
+ Parameters:
160
+
161
+ - `content`: required; either a string or `list[TextPart]`
162
+ - `font`: font family name, font file path, or webfont URL
163
+ - `size`: positive integer font size
164
+ - `color`: default text color for the layer
165
+ - `position`: optional `(x, y)` tuple using pixels or percentage strings
166
+ - `align`: optional `Align`, string alias, or `(horizontal, vertical)` tuple
167
+ - `bold`: legacy bold flag
168
+ - `italic`: italic flag
169
+ - `weight`: CSS-style font weight as int or name
170
+ - `max_width`: wrap width in pixels or percentage string
171
+ - `effects`: any mix of `Stroke`, `Shadow`, `Glow`, and text `Background`
172
+ - `line_height`: positive float
173
+ - `letter_spacing`: integer tracking adjustment
174
+ - `auto_scale`: shrink text until it fits `max_width`
175
+ - `rotation`: degrees
176
+ - `opacity`: float from `0.0` to `1.0`
177
+
178
+ Notes:
179
+
180
+ - `content` is required.
181
+ - `align` accepts values like `"center"`, `"top-left"`, `"bottom-right"`, or tuples like `("left", "middle")`.
182
+ - `font` may be a remote URL; QuickThumb downloads and caches it.
183
+ - `weight` is supported on both full text layers and individual `TextPart` entries.
184
+
185
+ ### Outline Layer
186
+
187
+ Adds a border around the full canvas.
188
+
189
+ ```python
190
+ canvas.outline(width=12, color="#B8FF00", offset=0, opacity=1.0)
191
+ ```
192
+
193
+ Parameters:
194
+
195
+ - `width`: positive integer
196
+ - `color`: hex string
197
+ - `offset`: non-negative integer inset/outset offset
198
+ - `opacity`: float from `0.0` to `1.0`
199
+
200
+ ### Shape Layer
201
+
202
+ Adds a positioned rectangle or ellipse.
203
+
204
+ ```python
205
+ from quickthumb import Canvas, Shadow, Stroke
206
+
207
+ canvas = Canvas(1280, 720).shape(
208
+ shape="rectangle",
209
+ position=(64, 64),
210
+ width=360,
211
+ height=120,
212
+ color="#CC0000",
213
+ border_radius=16,
214
+ opacity=0.95,
215
+ rotation=-4,
216
+ align=("left", "top"),
217
+ effects=[
218
+ Stroke(width=3, color="#FFFFFF"),
219
+ Shadow(offset_x=0, offset_y=10, color="#000000", blur_radius=18),
220
+ ],
221
+ )
222
+ ```
223
+
224
+ Parameters:
225
+
226
+ - `shape`: `"rectangle"` or `"ellipse"`
227
+ - `position`: required `(x, y)` tuple using pixels or percentage strings
228
+ - `width`: positive integer
229
+ - `height`: positive integer
230
+ - `color`: hex string
231
+ - `border_radius`: non-negative integer
232
+ - `opacity`: float from `0.0` to `1.0`
233
+ - `rotation`: degrees
234
+ - `align`: optional `Align`, string alias, or `(horizontal, vertical)` tuple
235
+ - `effects`: any mix of `Stroke`, `Shadow`, and `Glow`
236
+
237
+ ### Image Layer
238
+
239
+ Adds an overlay image or cutout.
240
+
241
+ ```python
242
+ from quickthumb import Canvas, Filter, Shadow
243
+
244
+ canvas = Canvas(1280, 720).image(
245
+ path="portrait.png",
246
+ position=("74%", "54%"),
247
+ width=420,
248
+ height=520,
249
+ fit="cover",
250
+ opacity=1.0,
251
+ rotation=0,
252
+ align=("center", "middle"),
253
+ remove_background=True,
254
+ border_radius=28,
255
+ blend_mode="normal",
256
+ effects=[
257
+ Filter(contrast=1.1, saturation=1.05),
258
+ Shadow(offset_x=0, offset_y=12, color="#000000", blur_radius=24),
259
+ ],
260
+ )
261
+ ```
262
+
263
+ Parameters:
264
+
265
+ - `path`: required local file path or remote URL
266
+ - `position`: required `(x, y)` tuple using pixels or percentage strings
267
+ - `width`: optional positive integer
268
+ - `height`: optional positive integer
269
+ - `fit`: `cover`, `contain`, or `fill` when width and height define a target box
270
+ - `opacity`: float from `0.0` to `1.0`
271
+ - `rotation`: degrees
272
+ - `align`: `Align`, string alias, or `(horizontal, vertical)` tuple
273
+ - `remove_background`: requires `quickthumb[rembg]`
274
+ - `border_radius`: non-negative integer
275
+ - `effects`: any mix of `Stroke`, `Shadow`, `Glow`, and `Filter`
276
+ - `blend_mode`: `multiply`, `overlay`, `screen`, `darken`, `lighten`, or `normal`
277
+
278
+ Notes:
279
+
280
+ - If only one of `width` or `height` is set, aspect ratio is preserved.
281
+ - `fit` matters when both `width` and `height` define a box.
282
+ - Image blend modes apply during compositing onto prior layers.
283
+
284
+ ### Custom Layer
285
+
286
+ Adds a callback that can draw directly onto the rendered Pillow image.
287
+
288
+ ```python
289
+ from PIL import ImageDraw
290
+ from quickthumb import Canvas
291
+
292
+ def add_badge(image):
293
+ draw = ImageDraw.Draw(image)
294
+ draw.polygon([(70, 70), (240, 70), (155, 180)], fill="#FF3B30")
295
+ return image
296
+
297
+ canvas = Canvas(512, 512).custom(add_badge)
298
+ ```
299
+
300
+ Named custom layers can be serialized to JSON by registering the function in a global registry:
301
+
302
+ ```python
303
+ from PIL import ImageDraw
304
+ from quickthumb import Canvas
305
+
306
+ def draw_bar(image, *, color: str = "#000000", height: int = 40) -> None:
307
+ ImageDraw.Draw(image).rectangle((0, 0, image.width, height), fill=color)
308
+
309
+ Canvas.register_layer_fn("draw_bar", draw_bar)
310
+
311
+ canvas = Canvas(512, 512).custom(draw_bar, name="draw_bar", kwargs={"color": "#E74C3C", "height": 60})
312
+
313
+ json_str = canvas.to_json()
314
+ # {"type": "custom", "name": "draw_bar", "kwargs": {"color": "#E74C3C", "height": 60}}
315
+
316
+ recreated = Canvas.from_json(json_str) # register_layer_fn must be called first
317
+ ```
318
+
319
+ Parameters:
320
+
321
+ - `fn`: required callable; receives a `PIL.Image.Image` as the first argument
322
+ - `name`: optional string; required for JSON serialization
323
+ - `kwargs`: optional dict of keyword arguments forwarded to `fn` at render time; must be JSON-serializable when `name` is set
324
+
325
+ Rules:
326
+
327
+ - `fn` must be callable.
328
+ - The callback receives a `PIL.Image.Image` as the first positional argument, followed by any `kwargs`.
329
+ - The callback may mutate and return the same image, return a new image of the same size, or return `None`.
330
+ - Exceptions from the callback are wrapped as `RenderingError`.
331
+ - Unnamed custom layers (`name=None`) cannot be serialized to JSON.
332
+ - `Canvas.register_layer_fn(name, fn)` must be called before `Canvas.from_json()` for any canvas containing a custom layer with that name.
333
+ - `Canvas.unregister_layer_fn(name)` removes a name from the registry.
334
+
335
+ ## Helpers, Enums, and Effects
336
+
337
+ ### Gradients
338
+
339
+ ```python
340
+ from quickthumb import LinearGradient, RadialGradient
341
+
342
+ LinearGradient(
343
+ angle=45,
344
+ stops=[("#FF5733", 0.0), ("#3333FF", 1.0)],
345
+ )
346
+
347
+ RadialGradient(
348
+ stops=[("#FF5733", 0.0), ("#3333FF", 1.0)],
349
+ center=(0.5, 0.5),
350
+ )
351
+ ```
352
+
353
+ ### TextPart
354
+
355
+ `TextPart` supports per-segment overrides inside rich text content.
356
+
357
+ ```python
358
+ from quickthumb import Stroke, TextPart
359
+
360
+ TextPart(
361
+ text="HOT",
362
+ color="#FF3B30",
363
+ size=56,
364
+ font="Impact",
365
+ bold=None,
366
+ italic=None,
367
+ weight=900,
368
+ line_height=None,
369
+ letter_spacing=0,
370
+ effects=[Stroke(width=3, color="#000000")],
371
+ )
372
+ ```
373
+
374
+ Rules:
375
+
376
+ - `text` cannot be empty.
377
+ - `weight` and `bold=True` are mutually exclusive here too.
378
+
379
+ ### Enums
380
+
381
+ Blend modes:
382
+
383
+ - `BlendMode.MULTIPLY`
384
+ - `BlendMode.OVERLAY`
385
+ - `BlendMode.SCREEN`
386
+ - `BlendMode.DARKEN`
387
+ - `BlendMode.LIGHTEN`
388
+ - `BlendMode.NORMAL`
389
+
390
+ Fit modes:
391
+
392
+ - `FitMode.COVER`
393
+ - `FitMode.CONTAIN`
394
+ - `FitMode.FILL`
395
+
396
+ Align values:
397
+
398
+ - `Align.CENTER`
399
+ - `Align.TOP_LEFT`
400
+ - `Align.TOP_CENTER`
401
+ - `Align.TOP_RIGHT`
402
+ - `Align.LEFT`
403
+ - `Align.RIGHT`
404
+ - `Align.BOTTOM_LEFT`
405
+ - `Align.BOTTOM_CENTER`
406
+ - `Align.BOTTOM_RIGHT`
407
+
408
+ ### Effects by Layer Type
409
+
410
+ - Text layers: `Stroke`, `Shadow`, `Glow`, `Background`
411
+ - Image layers: `Stroke`, `Shadow`, `Glow`, `Filter`
412
+ - Shape layers: `Stroke`, `Shadow`, `Glow`
413
+ - Background layers: `Filter`
414
+
415
+ ## JSON Schema
416
+
417
+ QuickThumb serializes canvases as a JSON object with top-level `width`, `height`, and `layers`.
418
+
419
+ ```json
420
+ {
421
+ "width": 1280,
422
+ "height": 720,
423
+ "layers": [
424
+ {
425
+ "type": "background",
426
+ "color": "#0F172A",
427
+ "opacity": 1.0,
428
+ "blend_mode": null,
429
+ "fit": null,
430
+ "effects": []
431
+ },
432
+ {
433
+ "type": "shape",
434
+ "shape": "rectangle",
435
+ "position": [48, 48],
436
+ "width": 320,
437
+ "height": 96,
438
+ "color": "#CC0000",
439
+ "border_radius": 14,
440
+ "opacity": 1.0,
441
+ "rotation": 0.0,
442
+ "align": "top-left",
443
+ "effects": [
444
+ { "type": "stroke", "width": 2, "color": "#FFFFFF" }
445
+ ]
446
+ },
447
+ {
448
+ "type": "text",
449
+ "content": [
450
+ { "text": "HELLO ", "color": "#FFFFFF", "effects": [] },
451
+ {
452
+ "text": "WORLD",
453
+ "color": "#B8FF00",
454
+ "weight": 900,
455
+ "effects": [
456
+ { "type": "stroke", "width": 3, "color": "#000000" }
457
+ ]
458
+ }
459
+ ],
460
+ "size": 88,
461
+ "position": ["8%", "50%"],
462
+ "align": "left",
463
+ "max_width": "60%",
464
+ "auto_scale": false,
465
+ "rotation": 0.0,
466
+ "opacity": 1.0,
467
+ "effects": [
468
+ {
469
+ "type": "shadow",
470
+ "offset_x": 4,
471
+ "offset_y": 4,
472
+ "color": "#000000",
473
+ "blur_radius": 8
474
+ }
475
+ ]
476
+ },
477
+ {
478
+ "type": "image",
479
+ "path": "portrait.png",
480
+ "position": ["74%", "54%"],
481
+ "width": 420,
482
+ "height": 520,
483
+ "opacity": 1.0,
484
+ "rotation": 0.0,
485
+ "remove_background": false,
486
+ "align": "center",
487
+ "border_radius": 24,
488
+ "fit": "cover",
489
+ "blend_mode": "normal",
490
+ "effects": [
491
+ { "type": "filter", "blur": 0, "brightness": 1.0, "contrast": 1.05, "saturation": 1.0 }
492
+ ]
493
+ },
494
+ {
495
+ "type": "outline",
496
+ "width": 12,
497
+ "color": "#B8FF00",
498
+ "offset": 0,
499
+ "opacity": 1.0
500
+ }
501
+ ]
502
+ }
503
+ ```
504
+
505
+ Serialization notes:
506
+
507
+ - Every layer uses a `type` discriminator.
508
+ - Enum-like fields serialize to strings.
509
+ - Positions serialize as JSON arrays.
510
+ - `align` serializes to a single string such as `"center"` or `"top-left"`.
511
+ - Named custom layers serialize as `{"type": "custom", "name": "...", "kwargs": {...}}`.
512
+ - Unnamed custom layers are not serializable.
513
+
514
+ ## Validation Rules and Gotchas
515
+
516
+ ### Text
517
+
518
+ - `content` is required for `canvas.text(...)`.
519
+ - Rich text lists cannot be empty.
520
+ - `TextPart.text` cannot be empty.
521
+ - `weight` and `bold=True` are mutually exclusive on both `TextLayer` and `TextPart`.
522
+ - `auto_scale=True` requires `max_width`.
523
+ - `max_width` must be a positive integer or positive percentage string.
524
+ - Percentage strings are validated for `position` and `max_width`.
525
+
526
+ ### Fonts
527
+
528
+ - `font` may be a font family name, font file path, or remote font URL.
529
+ - When `font` is a webfont URL, `bold`, `italic`, and `weight` flags are ignored.
530
+ - Provide separate font URLs for styled webfont variants.
531
+ - `QUICKTHUMB_FONT_DIR` can point QuickThumb to a custom font directory.
532
+ - `QUICKTHUMB_DEFAULT_FONT` can override the default fallback font.
533
+
534
+ ### Images and Shapes
535
+
536
+ - `image.position` and `shape.position` are required.
537
+ - Positions must be 2-item tuples or lists.
538
+ - Percentage positions may be negative, for example `("-10%", "50%")`.
539
+ - `border_radius` cannot be negative.
540
+ - `opacity` must be between `0.0` and `1.0`.
541
+
542
+ ### Rendering and Export
543
+
544
+ - `quality` is only supported for `JPEG` and `WEBP`; using it with `PNG` raises `RenderingError`.
545
+ - Unsupported file formats raise `RenderingError`.
546
+ - Invalid local paths or failed remote downloads raise rendering-time errors.
547
+
548
+ ### JSON
549
+
550
+ - `Canvas.from_json()` expects a JSON string.
551
+ - `Canvas.to_json()` raises `ValidationError` when the canvas contains unnamed custom layers.
552
+ - Named custom layers round-trip through JSON; `Canvas.register_layer_fn(name, fn)` must be called before `Canvas.from_json()`.
553
+ - `Canvas.from_json()` raises `ValidationError` if a custom layer name is not in the registry.
554
+ - Round-trip JSON works for all built-in layer types, effects, and named custom layers.
555
+
556
+ ## End-to-End Example
557
+
558
+ ```python
559
+ from quickthumb import Background, Canvas, Filter, Shadow, Stroke, TextPart
560
+
561
+ canvas = (
562
+ Canvas.from_aspect_ratio("16:9", base_width=1280)
563
+ .background(
564
+ image="https://images.unsplash.com/photo-1516321318423-f06f85e504b3",
565
+ fit="cover",
566
+ effects=[Filter(brightness=0.6)],
567
+ )
568
+ .background(color="#000000", opacity=0.35)
569
+ .shape(
570
+ shape="rectangle",
571
+ position=(52, 52),
572
+ width=360,
573
+ height=96,
574
+ color="#CC0000",
575
+ border_radius=14,
576
+ )
577
+ .text(
578
+ content=[
579
+ TextPart(text="AI ", color="#B8FF00", weight=900),
580
+ TextPart(text="THUMBNAILS", color="#FFFFFF", weight=900),
581
+ ],
582
+ size=108,
583
+ position=("8%", "52%"),
584
+ align=("left", "middle"),
585
+ max_width="58%",
586
+ effects=[
587
+ Stroke(width=6, color="#000000"),
588
+ Shadow(offset_x=4, offset_y=4, color="#000000", blur_radius=8),
589
+ Background(color="#111827CC", padding=(16, 22), border_radius=12),
590
+ ],
591
+ )
592
+ .image(
593
+ path="portrait.png",
594
+ position=("75%", "55%"),
595
+ width=430,
596
+ height=540,
597
+ fit="cover",
598
+ align=("center", "middle"),
599
+ border_radius=24,
600
+ effects=[Shadow(offset_x=0, offset_y=14, color="#000000", blur_radius=24)],
601
+ )
602
+ .outline(width=12, color="#B8FF00")
603
+ )
604
+
605
+ canvas.render("thumbnail.png")
606
+ ```
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quickthumb
3
- Version: 0.3.0
3
+ Version: 0.4.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
@@ -40,6 +40,10 @@ It is designed for code-first and JSON-first workflows, with a layer-based API t
40
40
  | --- | --- | --- |
41
41
  | ![YouTube thumbnail example](examples/youtube_thumbnail_01.png) | ![Burnout thumbnail example](examples/youtube_thumbnail_02.png) | ![Instagram news card example](examples/instagram_news_card.png) |
42
42
 
43
+ | Talking Head | Reaction / Commentary | Tutorial / Explainer |
44
+ | --- | --- | --- |
45
+ | ![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
+
43
47
  ## Why QuickThumb
44
48
 
45
49
  - Built for thumbnails and social graphics, not just generic image composition
@@ -240,7 +244,7 @@ serialized = canvas.to_json()
240
244
  Notes:
241
245
 
242
246
  - JSON uses top-level `width`, `height`, and `layers`
243
- - Custom layers added with `canvas.custom(fn)` are not JSON-serializable
247
+ - Named custom layers added with `canvas.custom(fn, name="...", kwargs={...})` are JSON-serializable via the registry; unnamed custom layers are not
244
248
  - Enum-like values such as `blend_mode`, `fit`, and `align` can be passed as strings
245
249
 
246
250
  ## AI-Friendly Workflows
@@ -301,7 +305,7 @@ os.environ["QUICKTHUMB_DEFAULT_FONT"] = "Roboto"
301
305
  | Image effects | Stroke, shadow, glow, filter effects, border radius, background removal |
302
306
  | Shapes | Rectangle and ellipse primitives with stroke/shadow/glow support |
303
307
  | Export | PNG, JPEG, WebP, file output, base64, data URLs |
304
- | Serialization | `to_json()` / `from_json()` for built-in layer types |
308
+ | Serialization | `to_json()` / `from_json()` for built-in layer types and named custom layers |
305
309
 
306
310
  ## Real Example Scripts
307
311
 
@@ -316,7 +320,7 @@ See the shipped examples in [`examples/README.md`](examples/README.md):
316
320
  - `weight` and `bold=True` are mutually exclusive on text layers and `TextPart`
317
321
  - `auto_scale=True` requires `max_width`
318
322
  - `position` percentage values must be strings like `"50%"`
319
- - `canvas.custom(fn)` runs during render order and cannot be serialized to JSON
323
+ - `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
320
324
 
321
325
  ## Development
322
326