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