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.
- quickthumb-0.4.0/.github/workflows/docs.yml +30 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/.gitignore +3 -0
- quickthumb-0.4.0/DESIGN.md +606 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/PKG-INFO +8 -4
- {quickthumb-0.3.0 → quickthumb-0.4.0}/README.md +7 -3
- quickthumb-0.4.0/docs/api/background.md +78 -0
- quickthumb-0.4.0/docs/api/canvas.md +141 -0
- quickthumb-0.4.0/docs/api/effects.md +195 -0
- quickthumb-0.4.0/docs/api/enums.md +194 -0
- quickthumb-0.4.0/docs/api/image.md +131 -0
- quickthumb-0.4.0/docs/api/index.md +43 -0
- quickthumb-0.4.0/docs/api/outline.md +61 -0
- quickthumb-0.4.0/docs/api/shape.md +110 -0
- quickthumb-0.4.0/docs/api/text.md +194 -0
- quickthumb-0.4.0/docs/assets/examples/instagram_news_card.png +0 -0
- quickthumb-0.4.0/docs/assets/examples/podcast_interview_promo.png +0 -0
- quickthumb-0.4.0/docs/assets/examples/shorts_cover_agent.png +0 -0
- quickthumb-0.4.0/docs/assets/examples/youtube_reaction.png +0 -0
- quickthumb-0.4.0/docs/assets/examples/youtube_talking_head.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0/docs/assets}/examples/youtube_thumbnail_02.png +0 -0
- quickthumb-0.4.0/docs/assets/examples/youtube_tutorial_explainer.png +0 -0
- quickthumb-0.4.0/docs/changelog.md +5 -0
- quickthumb-0.4.0/docs/concepts.md +255 -0
- quickthumb-0.4.0/docs/cookbook/ai-workflow.md +182 -0
- quickthumb-0.4.0/docs/cookbook/index.md +46 -0
- quickthumb-0.4.0/docs/cookbook/instagram-card.md +129 -0
- quickthumb-0.4.0/docs/cookbook/podcast-promo.md +214 -0
- quickthumb-0.4.0/docs/cookbook/shorts-cover.md +208 -0
- quickthumb-0.4.0/docs/cookbook/webfonts-rembg.md +199 -0
- quickthumb-0.4.0/docs/cookbook/youtube-thumbnail.md +348 -0
- quickthumb-0.4.0/docs/faq.md +170 -0
- quickthumb-0.4.0/docs/getting-started.md +206 -0
- quickthumb-0.4.0/docs/index.md +59 -0
- quickthumb-0.4.0/docs/installation.md +56 -0
- quickthumb-0.4.0/docs/json-schema.md +365 -0
- quickthumb-0.4.0/docs/stylesheets/extra.css +83 -0
- quickthumb-0.4.0/examples/README.md +155 -0
- quickthumb-0.4.0/examples/instagram_news_card.png +0 -0
- quickthumb-0.4.0/examples/podcast_interview_promo.png +0 -0
- quickthumb-0.4.0/examples/podcast_interview_promo.py +146 -0
- quickthumb-0.4.0/examples/shorts_cover_agent.json +302 -0
- quickthumb-0.4.0/examples/shorts_cover_agent.png +0 -0
- quickthumb-0.4.0/examples/shorts_cover_agent.py +33 -0
- quickthumb-0.4.0/examples/youtube_reaction.png +0 -0
- quickthumb-0.4.0/examples/youtube_reaction.py +103 -0
- quickthumb-0.4.0/examples/youtube_talking_head.png +0 -0
- quickthumb-0.4.0/examples/youtube_talking_head.py +102 -0
- quickthumb-0.4.0/examples/youtube_thumbnail_01.png +0 -0
- quickthumb-0.4.0/examples/youtube_thumbnail_02.png +0 -0
- quickthumb-0.4.0/examples/youtube_tutorial_explainer.png +0 -0
- quickthumb-0.4.0/examples/youtube_tutorial_explainer.py +136 -0
- quickthumb-0.4.0/mkdocs.yml +87 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/pyproject.toml +4 -1
- {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/canvas.py +190 -87
- quickthumb-0.4.0/tests/snapshots/custom_layer_with_kwargs.png +0 -0
- quickthumb-0.4.0/tests/snapshots/explicit_newlines_preserved_with_max_width.png +0 -0
- quickthumb-0.4.0/tests/snapshots/image_with_border_radius.png +0 -0
- quickthumb-0.4.0/tests/snapshots/image_with_drop_shadow.png +0 -0
- quickthumb-0.4.0/tests/snapshots/image_with_rotation.png +0 -0
- quickthumb-0.4.0/tests/snapshots/multiline_text_with_background.png +0 -0
- quickthumb-0.4.0/tests/snapshots/multiple_images.png +0 -0
- quickthumb-0.4.0/tests/snapshots/rich_text_with_background.png +0 -0
- quickthumb-0.4.0/tests/snapshots/shape_ellipse_basic.png +0 -0
- quickthumb-0.4.0/tests/snapshots/shape_rectangle_with_border_radius.png +0 -0
- quickthumb-0.4.0/tests/snapshots/shape_with_effects.png +0 -0
- quickthumb-0.4.0/tests/snapshots/shape_with_rotation.png +0 -0
- quickthumb-0.4.0/tests/snapshots/text_with_background_and_effects.png +0 -0
- quickthumb-0.4.0/tests/snapshots/text_with_background_basic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_canvas.py +113 -15
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_rendering.py +103 -3
- {quickthumb-0.3.0 → quickthumb-0.4.0}/uv.lock +396 -1
- quickthumb-0.3.0/DESIGN.md +0 -476
- quickthumb-0.3.0/PROGRESS.md +0 -123
- quickthumb-0.3.0/examples/README.md +0 -78
- quickthumb-0.3.0/examples/instagram_news_card.png +0 -0
- quickthumb-0.3.0/tests/snapshots/image_with_border_radius.png +0 -0
- quickthumb-0.3.0/tests/snapshots/image_with_drop_shadow.png +0 -0
- quickthumb-0.3.0/tests/snapshots/image_with_rotation.png +0 -0
- quickthumb-0.3.0/tests/snapshots/multiple_images.png +0 -0
- quickthumb-0.3.0/tests/snapshots/rich_text_with_background.png +0 -0
- quickthumb-0.3.0/tests/snapshots/shape_ellipse_basic.png +0 -0
- quickthumb-0.3.0/tests/snapshots/shape_rectangle_with_border_radius.png +0 -0
- quickthumb-0.3.0/tests/snapshots/shape_with_effects.png +0 -0
- quickthumb-0.3.0/tests/snapshots/shape_with_rotation.png +0 -0
- quickthumb-0.3.0/tests/snapshots/text_with_background_and_effects.png +0 -0
- quickthumb-0.3.0/tests/snapshots/text_with_background_basic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/.claude/settings.json +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/.claude/skills/tdd/SKILL.md +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/.github/workflows/publish.yaml +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/.vscode/extensions.json +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/.vscode/settings.json +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/CLAUDE.md +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/LICENSE +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Black.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-BlackItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Bold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-BoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ExtraBold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ExtraBoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ExtraLight.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ExtraLightItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Italic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Light.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-LightItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Medium.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-MediumItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Regular.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-SemiBold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-SemiBoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-Thin.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSans-ThinItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Black.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-BlackItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Bold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-BoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ExtraBold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ExtraBoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ExtraLight.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ExtraLightItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Italic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Light.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-LightItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Medium.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-MediumItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Regular.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-SemiBold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-SemiBoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-Thin.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/NotoSerif-ThinItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Black.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-BlackItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Bold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-BoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ExtraBold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ExtraBoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ExtraLight.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ExtraLightItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Italic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Light.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-LightItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Medium.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-MediumItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Regular.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-SemiBold.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-SemiBoldItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-Thin.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/fonts/Roboto-ThinItalic.ttf +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/images/c-g-JgDUVGAXsso-unsplash.jpg +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/images/denise-jans-WIRvXd1PYlg-unsplash.jpg +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/assets/images/tobias-rademacher-wnF27F85ZKw-unsplash.jpg +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0/docs/assets}/examples/youtube_thumbnail_01.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/examples/instagram_news_card.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/examples/youtube_thumbnail_01.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/examples/youtube_thumbnail_02.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/__init__.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/errors.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/font_cache.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/quickthumb/models.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/__init__.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/conftest.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/fixtures/ivana-cajina-_7LbC5J-jw4-unsplash.jpg +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/fixtures/sample_image.jpg +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/fixtures/tobias-rademacher-wnF27F85ZKw-unsplash.jpg +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/arial_bold.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/arial_bold_italic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/arial_italic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/auto_scale_rich_text.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/auto_scale_simple_text.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_blur_heavy.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_blur_soft.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_combined_filters.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_contrast_high.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_contrast_low.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_filter_effect.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_saturation_grayscale.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/background_saturation_vivid.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_darken.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_lighten.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_multiply.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_normal.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_overlay.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/blend_mode_screen.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/bold_italic_with_named_font.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/bold_with_named_font.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/custom_canvas_layer.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/font_name_works.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_background_basic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_background_with_opacity.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_basic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_darken.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_lighten.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_multiply.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_normal.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_overlay.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_blend_mode_screen.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_brightness_decrease.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_brightness_increase.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_filter_effect.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_fit_contain.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_fit_cover.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_fit_fill.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_on_background_image.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_overlay_fit_contain.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_overlay_fit_cover.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_overlay_fit_fill.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_percentage_position.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_glow.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_opacity.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_remove_background.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_stroke.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/image_with_text_overlay.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/italic_with_named_font.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/jpeg_format.jpg +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_brightness_decrease.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_brightness_increase.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_diagonal.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_horizontal.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_with_alpha.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/linear_gradient_with_opacity.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/multiline_text_with_glow.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/multiline_text_with_shadow.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/outline_basic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/outline_with_offset.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/percentage_with_alignment.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/radial_gradient_brightness_decrease.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/radial_gradient_brightness_increase.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/radial_gradient_centered.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/radial_gradient_with_opacity.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_advanced_styles.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_alignment.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_different_colors.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_mixed_fonts.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_mixed_styles.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/rich_text_with_effects.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/roboto_bold.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/roboto_bold_italic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/roboto_italic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/shape_rectangle_basic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/shape_with_opacity.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/solid_background.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/solid_color_brightness_decrease.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/solid_color_brightness_increase.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_alignment.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_and_outline_with_opacity.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_bold_and_italic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_no_wrapping.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_rendering.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_rotation_basic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_rotation_centered.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_rotation_with_effects.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_weight_black.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_weight_light.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_basic_shadow.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_blurred_shadow.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_glow.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_letter_spacing.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_letter_spacing_glow.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_letter_spacing_shadow.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_line_height.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_multiple_glows.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_with_stroke.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_wrapping_center_aligned.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_wrapping_center_aligned_percentage.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_wrapping_left_aligned.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/text_wrapping_right_aligned.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/times_new_roman_bold.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/times_new_roman_bold_italic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/times_new_roman_italic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/url_image_background_basic.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/webfont_basic_rendering.png +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/snapshots/webp_format.webp +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_background_layers.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_font_cache.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_image_layers.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_outline_layers.py +0 -0
- {quickthumb-0.3.0 → quickthumb-0.4.0}/tests/test_shape_layers.py +0 -0
- {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
|
|
@@ -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
|
+
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
|
|  |  |  |
|
|
42
42
|
|
|
43
|
+
| Talking Head | Reaction / Commentary | Tutorial / Explainer |
|
|
44
|
+
| --- | --- | --- |
|
|
45
|
+
|  |  |  |
|
|
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
|
-
-
|
|
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
|
|
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
|
|