quickthumb 0.2.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.
- quickthumb-0.3.1/.claude/skills/tdd/SKILL.md +29 -0
- quickthumb-0.3.1/.github/workflows/docs.yml +26 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/.gitignore +6 -0
- quickthumb-0.3.1/CLAUDE.md +30 -0
- quickthumb-0.3.1/DESIGN.md +573 -0
- quickthumb-0.3.1/PKG-INFO +337 -0
- quickthumb-0.3.1/PROGRESS.md +125 -0
- quickthumb-0.3.1/README.md +306 -0
- quickthumb-0.3.1/assets/images/denise-jans-WIRvXd1PYlg-unsplash.jpg +0 -0
- quickthumb-0.3.1/assets/images/tobias-rademacher-wnF27F85ZKw-unsplash.jpg +0 -0
- quickthumb-0.3.1/docs/api/background.md +78 -0
- quickthumb-0.3.1/docs/api/canvas.md +141 -0
- quickthumb-0.3.1/docs/api/effects.md +195 -0
- quickthumb-0.3.1/docs/api/enums.md +194 -0
- quickthumb-0.3.1/docs/api/image.md +131 -0
- quickthumb-0.3.1/docs/api/index.md +43 -0
- quickthumb-0.3.1/docs/api/outline.md +61 -0
- quickthumb-0.3.1/docs/api/shape.md +110 -0
- quickthumb-0.3.1/docs/api/text.md +194 -0
- quickthumb-0.3.1/docs/assets/examples/instagram_news_card.png +0 -0
- quickthumb-0.3.1/docs/assets/examples/podcast_interview_promo.png +0 -0
- quickthumb-0.3.1/docs/assets/examples/shorts_cover_agent.png +0 -0
- quickthumb-0.3.1/docs/assets/examples/youtube_reaction.png +0 -0
- quickthumb-0.3.1/docs/assets/examples/youtube_talking_head.png +0 -0
- quickthumb-0.3.1/docs/assets/examples/youtube_thumbnail_02.png +0 -0
- quickthumb-0.3.1/docs/assets/examples/youtube_tutorial_explainer.png +0 -0
- quickthumb-0.3.1/docs/changelog.md +5 -0
- quickthumb-0.3.1/docs/concepts.md +255 -0
- quickthumb-0.3.1/docs/cookbook/ai-workflow.md +182 -0
- quickthumb-0.3.1/docs/cookbook/index.md +46 -0
- quickthumb-0.3.1/docs/cookbook/instagram-card.md +129 -0
- quickthumb-0.3.1/docs/cookbook/podcast-promo.md +214 -0
- quickthumb-0.3.1/docs/cookbook/shorts-cover.md +208 -0
- quickthumb-0.3.1/docs/cookbook/webfonts-rembg.md +199 -0
- quickthumb-0.3.1/docs/cookbook/youtube-thumbnail.md +348 -0
- quickthumb-0.3.1/docs/faq.md +170 -0
- quickthumb-0.3.1/docs/getting-started.md +206 -0
- quickthumb-0.3.1/docs/index.md +59 -0
- quickthumb-0.3.1/docs/installation.md +56 -0
- quickthumb-0.3.1/docs/json-schema.md +365 -0
- quickthumb-0.3.1/docs/stylesheets/extra.css +83 -0
- quickthumb-0.3.1/examples/README.md +155 -0
- quickthumb-0.3.1/examples/instagram_news_card.png +0 -0
- quickthumb-0.3.1/examples/instagram_news_card.py +94 -0
- quickthumb-0.3.1/examples/podcast_interview_promo.png +0 -0
- quickthumb-0.3.1/examples/podcast_interview_promo.py +146 -0
- quickthumb-0.3.1/examples/shorts_cover_agent.json +301 -0
- quickthumb-0.3.1/examples/shorts_cover_agent.png +0 -0
- quickthumb-0.3.1/examples/shorts_cover_agent.py +33 -0
- quickthumb-0.3.1/examples/youtube_reaction.png +0 -0
- quickthumb-0.3.1/examples/youtube_reaction.py +103 -0
- quickthumb-0.3.1/examples/youtube_talking_head.png +0 -0
- quickthumb-0.3.1/examples/youtube_talking_head.py +102 -0
- quickthumb-0.3.1/examples/youtube_thumbnail_01.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/examples/youtube_thumbnail_01.py +11 -5
- quickthumb-0.3.1/examples/youtube_thumbnail_02.png +0 -0
- quickthumb-0.3.1/examples/youtube_thumbnail_02.py +109 -0
- quickthumb-0.3.1/examples/youtube_tutorial_explainer.png +0 -0
- quickthumb-0.3.1/examples/youtube_tutorial_explainer.py +136 -0
- quickthumb-0.3.1/mkdocs.yml +87 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/pyproject.toml +39 -2
- {quickthumb-0.2.0 → quickthumb-0.3.1}/quickthumb/__init__.py +16 -0
- quickthumb-0.3.1/quickthumb/canvas.py +2406 -0
- quickthumb-0.3.1/quickthumb/errors.py +19 -0
- quickthumb-0.3.1/quickthumb/models.py +497 -0
- quickthumb-0.3.1/tests/fixtures/ivana-cajina-_7LbC5J-jw4-unsplash.jpg +0 -0
- quickthumb-0.3.1/tests/fixtures/tobias-rademacher-wnF27F85ZKw-unsplash.jpg +0 -0
- quickthumb-0.3.1/tests/snapshots/auto_scale_rich_text.png +0 -0
- quickthumb-0.3.1/tests/snapshots/auto_scale_simple_text.png +0 -0
- quickthumb-0.3.1/tests/snapshots/background_blur_heavy.png +0 -0
- quickthumb-0.3.1/tests/snapshots/background_blur_soft.png +0 -0
- quickthumb-0.3.1/tests/snapshots/background_combined_filters.png +0 -0
- quickthumb-0.3.1/tests/snapshots/background_contrast_high.png +0 -0
- quickthumb-0.3.1/tests/snapshots/background_contrast_low.png +0 -0
- quickthumb-0.3.1/tests/snapshots/background_filter_effect.png +0 -0
- quickthumb-0.3.1/tests/snapshots/background_saturation_grayscale.png +0 -0
- quickthumb-0.3.1/tests/snapshots/background_saturation_vivid.png +0 -0
- quickthumb-0.3.1/tests/snapshots/custom_canvas_layer.png +0 -0
- quickthumb-0.3.1/tests/snapshots/explicit_newlines_preserved_with_max_width.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_basic.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_blend_mode_darken.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_blend_mode_lighten.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_blend_mode_multiply.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_blend_mode_normal.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_blend_mode_overlay.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_blend_mode_screen.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_filter_effect.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_on_background_image.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_overlay_fit_contain.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_overlay_fit_cover.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_overlay_fit_fill.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_percentage_position.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_with_border_radius.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_with_drop_shadow.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_with_glow.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_with_opacity.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_with_remove_background.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_with_rotation.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_with_stroke.png +0 -0
- quickthumb-0.3.1/tests/snapshots/image_with_text_overlay.png +0 -0
- quickthumb-0.3.1/tests/snapshots/linear_gradient_with_alpha.png +0 -0
- quickthumb-0.3.1/tests/snapshots/multiline_text_with_background.png +0 -0
- quickthumb-0.3.1/tests/snapshots/multiline_text_with_glow.png +0 -0
- quickthumb-0.3.1/tests/snapshots/multiline_text_with_shadow.png +0 -0
- quickthumb-0.3.1/tests/snapshots/multiple_images.png +0 -0
- quickthumb-0.3.1/tests/snapshots/rich_text_with_background.png +0 -0
- quickthumb-0.3.1/tests/snapshots/shape_ellipse_basic.png +0 -0
- quickthumb-0.3.1/tests/snapshots/shape_rectangle_basic.png +0 -0
- quickthumb-0.3.1/tests/snapshots/shape_rectangle_with_border_radius.png +0 -0
- quickthumb-0.3.1/tests/snapshots/shape_with_effects.png +0 -0
- quickthumb-0.3.1/tests/snapshots/shape_with_opacity.png +0 -0
- quickthumb-0.3.1/tests/snapshots/shape_with_rotation.png +0 -0
- quickthumb-0.3.1/tests/snapshots/text_and_outline_with_opacity.png +0 -0
- quickthumb-0.3.1/tests/snapshots/text_rotation_basic.png +0 -0
- quickthumb-0.3.1/tests/snapshots/text_rotation_centered.png +0 -0
- quickthumb-0.3.1/tests/snapshots/text_rotation_with_effects.png +0 -0
- quickthumb-0.3.1/tests/snapshots/text_with_background_and_effects.png +0 -0
- quickthumb-0.3.1/tests/snapshots/text_with_background_basic.png +0 -0
- quickthumb-0.3.1/tests/snapshots/text_with_letter_spacing_glow.png +0 -0
- quickthumb-0.3.1/tests/snapshots/text_with_letter_spacing_shadow.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/test_background_layers.py +53 -44
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/test_canvas.py +92 -146
- quickthumb-0.3.1/tests/test_image_layers.py +555 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/test_outline_layers.py +5 -4
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/test_rendering.py +1148 -9
- quickthumb-0.3.1/tests/test_shape_layers.py +280 -0
- quickthumb-0.3.1/tests/test_text_layers.py +1449 -0
- quickthumb-0.3.1/uv.lock +2042 -0
- quickthumb-0.2.0/.claude/agents/tdd-agent.md +0 -10
- quickthumb-0.2.0/CLAUDE.md +0 -57
- quickthumb-0.2.0/DESIGN.md +0 -480
- quickthumb-0.2.0/PKG-INFO +0 -286
- quickthumb-0.2.0/PROGRESS.md +0 -79
- quickthumb-0.2.0/README.md +0 -273
- quickthumb-0.2.0/examples/README.md +0 -18
- quickthumb-0.2.0/quickthumb/canvas.py +0 -1362
- quickthumb-0.2.0/quickthumb/errors.py +0 -10
- quickthumb-0.2.0/quickthumb/models.py +0 -379
- quickthumb-0.2.0/tests/test_text_effects.py +0 -413
- quickthumb-0.2.0/tests/test_text_layers.py +0 -613
- quickthumb-0.2.0/uv.lock +0 -626
- {quickthumb-0.2.0 → quickthumb-0.3.1}/.claude/settings.json +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/.github/workflows/publish.yaml +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/.vscode/extensions.json +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/.vscode/settings.json +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/LICENSE +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-Black.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-BlackItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-Bold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-BoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-ExtraBold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-ExtraBoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-ExtraLight.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-ExtraLightItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-Italic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-Light.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-LightItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-Medium.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-MediumItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-Regular.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-SemiBold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-SemiBoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-Thin.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSans-ThinItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-Black.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-BlackItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-Bold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-BoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-ExtraBold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-ExtraBoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-ExtraLight.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-ExtraLightItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-Italic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-Light.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-LightItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-Medium.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-MediumItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-Regular.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-SemiBold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-SemiBoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-Thin.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/NotoSerif-ThinItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-Black.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-BlackItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-Bold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-BoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-ExtraBold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-ExtraBoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-ExtraLight.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-ExtraLightItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-Italic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-Light.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-LightItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-Medium.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-MediumItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-Regular.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-SemiBold.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-SemiBoldItalic.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-Thin.ttf +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/assets/fonts/Roboto-ThinItalic.ttf +0 -0
- {quickthumb-0.2.0/examples/assets → quickthumb-0.3.1/assets/images}/c-g-JgDUVGAXsso-unsplash.jpg +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1/docs/assets}/examples/youtube_thumbnail_01.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/quickthumb/font_cache.py +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/__init__.py +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/conftest.py +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/fixtures/sample_image.jpg +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/arial_bold.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/arial_bold_italic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/arial_italic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/blend_mode_darken.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/blend_mode_lighten.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/blend_mode_multiply.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/blend_mode_normal.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/blend_mode_overlay.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/blend_mode_screen.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/bold_italic_with_named_font.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/bold_with_named_font.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/font_name_works.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/image_background_basic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/image_background_with_opacity.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/image_brightness_decrease.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/image_brightness_increase.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/image_fit_contain.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/image_fit_cover.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/image_fit_fill.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/italic_with_named_font.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/jpeg_format.jpg +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/linear_gradient_brightness_decrease.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/linear_gradient_brightness_increase.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/linear_gradient_diagonal.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/linear_gradient_horizontal.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/linear_gradient_with_opacity.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/outline_basic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/outline_with_offset.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/percentage_with_alignment.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/radial_gradient_brightness_decrease.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/radial_gradient_brightness_increase.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/radial_gradient_centered.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/radial_gradient_with_opacity.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/rich_text_advanced_styles.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/rich_text_alignment.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/rich_text_different_colors.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/rich_text_mixed_fonts.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/rich_text_mixed_styles.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/rich_text_with_effects.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/roboto_bold.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/roboto_bold_italic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/roboto_italic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/solid_background.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/solid_color_brightness_decrease.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/solid_color_brightness_increase.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_alignment.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_bold_and_italic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_no_wrapping.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_rendering.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_weight_black.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_weight_light.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_with_basic_shadow.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_with_blurred_shadow.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_with_glow.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_with_letter_spacing.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_with_line_height.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_with_multiple_glows.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_with_stroke.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_wrapping_center_aligned.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_wrapping_center_aligned_percentage.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_wrapping_left_aligned.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/text_wrapping_right_aligned.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/times_new_roman_bold.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/times_new_roman_bold_italic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/times_new_roman_italic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/url_image_background_basic.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/webfont_basic_rendering.png +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/snapshots/webp_format.webp +0 -0
- {quickthumb-0.2.0 → quickthumb-0.3.1}/tests/test_font_cache.py +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
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
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Workflow
|
|
8
|
+
|
|
9
|
+
```mermaid
|
|
10
|
+
graph TD
|
|
11
|
+
RED[RED: Write failing test] --> REVIEW[Spawn subagent: devil's advocate test review]
|
|
12
|
+
REVIEW -->|Pass| GREEN[GREEN: Write minimal code to pass]
|
|
13
|
+
REVIEW -->|Fail| RED
|
|
14
|
+
GREEN --> REFACTOR[REFACTOR: Improve code/tests or state 'No refactoring needed']
|
|
15
|
+
REFACTOR --> RED
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The devil's advocate subagent reviews test quality against the checklists below before implementation begins.
|
|
19
|
+
|
|
20
|
+
## Checklists
|
|
21
|
+
|
|
22
|
+
1. Follow the "Detroit School" of TDD (Classical TDD). Never mock internal methods or nearby collaborators.
|
|
23
|
+
2. Use `inline_snapshot` for complex object state verification instead of partial field assertions.
|
|
24
|
+
3. Prefer fewer, higher-value tests: One real integration test beats three mock-based unit tests.
|
|
25
|
+
4. Drop tests covered by type checker/linter
|
|
26
|
+
5. Parametrize `ValidationError` cases into one test.
|
|
27
|
+
6. Tests belong in their domain file, not a per-feature file. Visual effect snapshots go in `test_rendering.py`; use real image fixtures so differences are obvious to reviewers.
|
|
28
|
+
7. Snapshot tests subsume pixel-assertion behavioral tests — drop the latter when snapshots exist. Before adding a test, check existing coverage; only add if a genuinely new contract is introduced.
|
|
29
|
+
8. Extend an existing relevant test to cover a new concern rather than adding a new test — new tests are justified only when no existing test can naturally absorb the change.
|
|
@@ -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
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code when working with this repository.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
QuickThumb is a Python library for programmatic thumbnail generation. See @README.md for features, @DESIGN.md for API specifications.
|
|
8
|
+
|
|
9
|
+
## Project Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
quickthumb/
|
|
13
|
+
├── canvas.py # Canvas class with method chaining API
|
|
14
|
+
├── models.py # Pydantic models (CanvasModel, BackgroundLayer, TextLayer, etc.)
|
|
15
|
+
├── font_cache.py # Font loading and caching
|
|
16
|
+
└── errors.py # Custom exceptions
|
|
17
|
+
tests/ # Tests follow pattern: test_{component}.py
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Development Commands
|
|
21
|
+
|
|
22
|
+
### Setup
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# Install dependencies
|
|
26
|
+
uv sync
|
|
27
|
+
|
|
28
|
+
# Tests
|
|
29
|
+
uv run pytest [args]
|
|
30
|
+
```
|
|
@@ -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
|
+
```
|