diff --git a/CLAUDE.md b/CLAUDE.md deleted file mode 100644 index b6c470d..0000000 --- a/CLAUDE.md +++ /dev/null @@ -1,30 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code when working with this repository. - -## Project Overview - -QuickThumb is a Python library for programmatic thumbnail generation. See @README.md for features and API specifications, @specs/SPEC.md for planned features. - -## Project Structure - -``` -quickthumb/ -├── canvas.py # Canvas class with method chaining API -├── models.py # Pydantic models (CanvasModel, BackgroundLayer, TextLayer, etc.) -├── font_cache.py # Font loading and caching -└── errors.py # Custom exceptions -tests/ # Tests follow pattern: test_{component}.py -``` - -## Development Commands - -### Setup - -```bash -# Install dependencies -uv sync - -# Tests -uv run pytest [args] -``` diff --git a/README.md b/README.md index 1ae3d1f..2a8d3a5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# QuickThumb +# quickthumb -QuickThumb is a Python library for programmatic thumbnail, social card, and promo image generation. +quickthumb is a Python library for programmatic thumbnail, social card, and promo image generation. It is designed for code-first and JSON-first workflows, with a layer-based API that works well for human-authored scripts and AI-generated specs. ## Gallery @@ -13,7 +13,7 @@ It is designed for code-first and JSON-first workflows, with a layer-based API t | --- | --- | --- | | ![Talking head thumbnail example](examples/youtube_talking_head.png) | ![Reaction thumbnail example](examples/youtube_reaction.png) | ![Tutorial thumbnail example](examples/youtube_tutorial_explainer.png) | -## Why QuickThumb +## Why quickthumb - Built for thumbnails and social graphics, not just generic image composition - Works with Python method chaining and JSON serialization/deserialization @@ -259,7 +259,7 @@ canvas.render("output.webp", format="WEBP", quality=90) ## JSON-First Workflow -QuickThumb can round-trip most canvases through JSON: +quickthumb can round-trip most canvases through JSON: ```python from quickthumb import Canvas @@ -275,7 +275,7 @@ config = """ }, { "type": "text", - "content": "Hello QuickThumb", + "content": "Hello quickthumb", "size": 72, "color": "#FFFFFF", "align": "center", @@ -304,12 +304,12 @@ Notes: ## AI-Friendly Workflows -QuickThumb is a good target when you want an LLM to generate image specs that are deterministic and easy to validate. +quickthumb is a good target when you want an LLM to generate image specs that are deterministic and easy to validate. Prompt pattern for Python generation: ```text -Generate QuickThumb Python code for a 1280x720 YouTube thumbnail. +Generate quickthumb Python code for a 1280x720 YouTube thumbnail. Use layered composition only. Keep text on the left, subject image on the right, and use high-contrast typography. Return runnable code that ends with canvas.render("thumbnail.png"). @@ -318,20 +318,20 @@ Return runnable code that ends with canvas.render("thumbnail.png"). Prompt pattern for JSON generation: ```text -Generate a QuickThumb JSON config with top-level width, height, and layers. +Generate a quickthumb JSON config with top-level width, height, and layers. Use one background image layer, one dark overlay background layer, two text layers, and one outline layer. -Only use valid QuickThumb layer types and effect names. +Only use valid quickthumb layer types and effect names. ``` Recommended workflow: -1. Have the model produce QuickThumb Python or JSON. +1. Have the model produce quickthumb Python or JSON. 2. Validate or render it locally. 3. Adjust only the content, colors, and assets instead of rewriting layout logic from scratch. ## Environment Variables -QuickThumb looks for fonts using these environment variables: +quickthumb looks for fonts using these environment variables: - `QUICKTHUMB_FONT_DIR`: directory that contains font files - `QUICKTHUMB_DEFAULT_FONT`: default font family/name to use when `font` is omitted diff --git a/assets/brand/apple-touch-icon.png b/assets/brand/apple-touch-icon.png new file mode 100644 index 0000000..11269d0 Binary files /dev/null and b/assets/brand/apple-touch-icon.png differ diff --git a/assets/brand/favicon.ico b/assets/brand/favicon.ico new file mode 100644 index 0000000..3b14fd7 Binary files /dev/null and b/assets/brand/favicon.ico differ diff --git a/assets/brand/favicon.png b/assets/brand/favicon.png new file mode 100644 index 0000000..358ce17 Binary files /dev/null and b/assets/brand/favicon.png differ diff --git a/assets/brand/quickthumb-icon-1024.png b/assets/brand/quickthumb-icon-1024.png new file mode 100644 index 0000000..7789213 Binary files /dev/null and b/assets/brand/quickthumb-icon-1024.png differ diff --git a/assets/brand/quickthumb-icon-152.png b/assets/brand/quickthumb-icon-152.png new file mode 100644 index 0000000..9ba7439 Binary files /dev/null and b/assets/brand/quickthumb-icon-152.png differ diff --git a/assets/brand/quickthumb-icon-16.png b/assets/brand/quickthumb-icon-16.png new file mode 100644 index 0000000..022e8b8 Binary files /dev/null and b/assets/brand/quickthumb-icon-16.png differ diff --git a/assets/brand/quickthumb-icon-167.png b/assets/brand/quickthumb-icon-167.png new file mode 100644 index 0000000..818ee7d Binary files /dev/null and b/assets/brand/quickthumb-icon-167.png differ diff --git a/assets/brand/quickthumb-icon-180.png b/assets/brand/quickthumb-icon-180.png new file mode 100644 index 0000000..11269d0 Binary files /dev/null and b/assets/brand/quickthumb-icon-180.png differ diff --git a/assets/brand/quickthumb-icon-192.png b/assets/brand/quickthumb-icon-192.png new file mode 100644 index 0000000..3909b0b Binary files /dev/null and b/assets/brand/quickthumb-icon-192.png differ diff --git a/assets/brand/quickthumb-icon-256.png b/assets/brand/quickthumb-icon-256.png new file mode 100644 index 0000000..228d4f5 Binary files /dev/null and b/assets/brand/quickthumb-icon-256.png differ diff --git a/assets/brand/quickthumb-icon-32.png b/assets/brand/quickthumb-icon-32.png new file mode 100644 index 0000000..358ce17 Binary files /dev/null and b/assets/brand/quickthumb-icon-32.png differ diff --git a/assets/brand/quickthumb-icon-48.png b/assets/brand/quickthumb-icon-48.png new file mode 100644 index 0000000..73ad16b Binary files /dev/null and b/assets/brand/quickthumb-icon-48.png differ diff --git a/assets/brand/quickthumb-icon-512.png b/assets/brand/quickthumb-icon-512.png new file mode 100644 index 0000000..c26339b Binary files /dev/null and b/assets/brand/quickthumb-icon-512.png differ diff --git a/assets/brand/quickthumb-icon-64.png b/assets/brand/quickthumb-icon-64.png new file mode 100644 index 0000000..e5ce342 Binary files /dev/null and b/assets/brand/quickthumb-icon-64.png differ diff --git a/assets/brand/quickthumb-icon-master-transparent.png b/assets/brand/quickthumb-icon-master-transparent.png new file mode 100644 index 0000000..67043e8 Binary files /dev/null and b/assets/brand/quickthumb-icon-master-transparent.png differ diff --git a/assets/brand/quickthumb-icon-master.png b/assets/brand/quickthumb-icon-master.png new file mode 100644 index 0000000..14f563c Binary files /dev/null and b/assets/brand/quickthumb-icon-master.png differ diff --git a/docs/api/background.md b/docs/api/background.md index eb70690..fb94e9e 100644 --- a/docs/api/background.md +++ b/docs/api/background.md @@ -1,5 +1,5 @@ --- -description: Reference for QuickThumb background layers, including colors, gradients, images, opacity, effects, and render order. +description: Reference for quickthumb background layers, including colors, gradients, images, opacity, effects, and render order. --- # Background diff --git a/docs/api/canvas.md b/docs/api/canvas.md index 7feba37..fdf37e3 100644 --- a/docs/api/canvas.md +++ b/docs/api/canvas.md @@ -1,5 +1,5 @@ --- -description: Reference for QuickThumb Canvas creation, aspect ratios, layer composition, rendering, JSON loading, and image output. +description: Reference for quickthumb Canvas creation, aspect ratios, layer composition, rendering, JSON loading, and image output. --- # Canvas diff --git a/docs/api/effects.md b/docs/api/effects.md index 135a4eb..1675f7c 100644 --- a/docs/api/effects.md +++ b/docs/api/effects.md @@ -1,5 +1,5 @@ --- -description: Reference for QuickThumb effects, including filters, shadows, strokes, blend modes, opacity, and layer compatibility. +description: Reference for quickthumb effects, including filters, shadows, strokes, blend modes, opacity, and layer compatibility. --- # Effects diff --git a/docs/api/enums.md b/docs/api/enums.md index 2e1dfdb..919d83a 100644 --- a/docs/api/enums.md +++ b/docs/api/enums.md @@ -1,5 +1,5 @@ --- -description: Reference for QuickThumb enums and gradient models used for alignment, fit modes, shapes, filters, and color transitions. +description: Reference for quickthumb enums and gradient models used for alignment, fit modes, shapes, filters, and color transitions. --- # Enums & Gradients diff --git a/docs/api/image.md b/docs/api/image.md index 054b851..e5ea47c 100644 --- a/docs/api/image.md +++ b/docs/api/image.md @@ -1,5 +1,5 @@ --- -description: Reference for QuickThumb image layers, including local and remote images, fit modes, positioning, masking, effects, and background removal. +description: Reference for quickthumb image layers, including local and remote images, fit modes, positioning, masking, effects, and background removal. --- # Image diff --git a/docs/api/index.md b/docs/api/index.md index 860f4e5..be5e085 100644 --- a/docs/api/index.md +++ b/docs/api/index.md @@ -1,10 +1,10 @@ --- -description: Explore the QuickThumb API reference for Canvas, layers, backgrounds, text, images, shapes, outlines, effects, enums, and gradients. +description: Explore the quickthumb API reference for Canvas, layers, backgrounds, text, images, shapes, outlines, effects, enums, and gradients. --- # API Reference -Complete reference for every class, method, and parameter in QuickThumb. +Complete reference for every class, method, and parameter in quickthumb. ## Public imports diff --git a/docs/api/outline.md b/docs/api/outline.md index 5d12c61..e46ef03 100644 --- a/docs/api/outline.md +++ b/docs/api/outline.md @@ -1,5 +1,5 @@ --- -description: Reference for QuickThumb outline layers that draw canvas borders with configurable width, color, opacity, and corner radius. +description: Reference for quickthumb outline layers that draw canvas borders with configurable width, color, opacity, and corner radius. --- # Outline diff --git a/docs/api/shape.md b/docs/api/shape.md index 7bf976c..c3ea277 100644 --- a/docs/api/shape.md +++ b/docs/api/shape.md @@ -1,5 +1,5 @@ --- -description: Reference for QuickThumb shape layers, including rectangles, ellipses, fills, gradients, strokes, shadows, and positioning. +description: Reference for quickthumb shape layers, including rectangles, ellipses, fills, gradients, strokes, shadows, and positioning. --- # Shape diff --git a/docs/api/text.md b/docs/api/text.md index 2a897ae..bc448e6 100644 --- a/docs/api/text.md +++ b/docs/api/text.md @@ -1,5 +1,5 @@ --- -description: Reference for QuickThumb text layers, including rich text parts, fonts, alignment, wrapping, strokes, shadows, and sizing. +description: Reference for quickthumb text layers, including rich text parts, fonts, alignment, wrapping, strokes, shadows, and sizing. --- # Text diff --git a/docs/assets/brand/apple-touch-icon.png b/docs/assets/brand/apple-touch-icon.png new file mode 100644 index 0000000..11269d0 Binary files /dev/null and b/docs/assets/brand/apple-touch-icon.png differ diff --git a/docs/assets/brand/favicon.ico b/docs/assets/brand/favicon.ico new file mode 100644 index 0000000..3b14fd7 Binary files /dev/null and b/docs/assets/brand/favicon.ico differ diff --git a/docs/assets/brand/favicon.png b/docs/assets/brand/favicon.png new file mode 100644 index 0000000..358ce17 Binary files /dev/null and b/docs/assets/brand/favicon.png differ diff --git a/docs/assets/brand/quickthumb-icon-192.png b/docs/assets/brand/quickthumb-icon-192.png new file mode 100644 index 0000000..3909b0b Binary files /dev/null and b/docs/assets/brand/quickthumb-icon-192.png differ diff --git a/docs/assets/brand/quickthumb-icon-512.png b/docs/assets/brand/quickthumb-icon-512.png new file mode 100644 index 0000000..c26339b Binary files /dev/null and b/docs/assets/brand/quickthumb-icon-512.png differ diff --git a/docs/assets/brand/quickthumb-icon.png b/docs/assets/brand/quickthumb-icon.png new file mode 100644 index 0000000..c26339b Binary files /dev/null and b/docs/assets/brand/quickthumb-icon.png differ diff --git a/docs/assets/brand/social-preview.png b/docs/assets/brand/social-preview.png new file mode 100644 index 0000000..a862354 Binary files /dev/null and b/docs/assets/brand/social-preview.png differ diff --git a/docs/changelog.md b/docs/changelog.md index e21a679..91c8fb7 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,5 +1,5 @@ --- -description: Review QuickThumb release notes and follow version changes, fixes, and new features through GitHub Releases. +description: Review quickthumb release notes and follow version changes, fixes, and new features through GitHub Releases. --- # Changelog diff --git a/docs/concepts.md b/docs/concepts.md index fa094da..4132d24 100644 --- a/docs/concepts.md +++ b/docs/concepts.md @@ -1,5 +1,5 @@ --- -description: Learn QuickThumb's core model for canvases, ordered layers, coordinates, alignment, effects, fonts, rendering, and JSON specs. +description: Learn quickthumb's core model for canvases, ordered layers, coordinates, alignment, effects, fonts, rendering, and JSON specs. --- # Core Concepts @@ -245,7 +245,7 @@ canvas2 = Canvas.from_json(json_str) ## Validation -QuickThumb validates all inputs at construction time using Pydantic. Invalid values raise `ValidationError` immediately, before any rendering occurs. +quickthumb validates all inputs at construction time using Pydantic. Invalid values raise `ValidationError` immediately, before any rendering occurs. ```python from quickthumb import ValidationError diff --git a/docs/cookbook/ai-workflow.md b/docs/cookbook/ai-workflow.md index 0baa038..88ad80d 100644 --- a/docs/cookbook/ai-workflow.md +++ b/docs/cookbook/ai-workflow.md @@ -1,12 +1,12 @@ --- -description: Generate reliable QuickThumb JSON specs with LLMs, validate them, render images, and iterate on AI-assisted thumbnail workflows. +description: Generate reliable quickthumb JSON specs with LLMs, validate them, render images, and iterate on AI-assisted thumbnail workflows. --- # AI Workflow -QuickThumb is designed to be a reliable target for LLM-generated image specs. This recipe walks through a full end-to-end workflow: writing a prompt, getting output, rendering it, and iterating. +quickthumb is designed to be a reliable target for LLM-generated image specs. This recipe walks through a full end-to-end workflow: writing a prompt, getting output, rendering it, and iterating. -## Why QuickThumb works well with AI +## Why quickthumb works well with AI - The schema is flat and typed — no nested ambiguity - Every layer has a required `type` discriminator @@ -18,7 +18,7 @@ QuickThumb is designed to be a reliable target for LLM-generated image specs. Th ``` 1. Write a prompt describing the layout -2. Model outputs QuickThumb JSON or Python +2. Model outputs quickthumb JSON or Python 3. Validate locally (ValidationError catches bad specs immediately) 4. Render to a PNG file 5. Review and iterate with targeted instructions @@ -29,7 +29,7 @@ QuickThumb is designed to be a reliable target for LLM-generated image specs. Th ### For JSON output ```text -Generate a QuickThumb JSON config for a 1280×720 YouTube thumbnail. +Generate a quickthumb JSON config for a 1280×720 YouTube thumbnail. Schema rules: - Top-level fields: "width", "height", "layers" @@ -51,7 +51,7 @@ Return only the JSON object. No explanation, no markdown fencing. ### For Python output ```text -Generate QuickThumb Python code for a 1280×720 YouTube thumbnail. +Generate quickthumb Python code for a 1280×720 YouTube thumbnail. Available imports: from quickthumb import ( @@ -117,7 +117,7 @@ Image(data=base64.b64decode(b64)) Instead of regenerating the whole spec, give the model the current spec and a specific change: ```text -Here is my current QuickThumb JSON spec: +Here is my current quickthumb JSON spec: diff --git a/docs/cookbook/index.md b/docs/cookbook/index.md index 1a2400b..71f1e86 100644 --- a/docs/cookbook/index.md +++ b/docs/cookbook/index.md @@ -1,10 +1,10 @@ --- -description: Browse ready-to-run QuickThumb recipes for YouTube thumbnails, Instagram cards, podcast promos, shorts covers, and AI workflows. +description: Browse ready-to-run quickthumb recipes for YouTube thumbnails, Instagram cards, podcast promos, shorts covers, and AI workflows. --- # Cookbook -Ready-to-run examples for common thumbnail and social card formats. Each recipe uses real QuickThumb code you can copy, adapt, and run locally. +Ready-to-run examples for common thumbnail and social card formats. Each recipe uses real quickthumb code you can copy, adapt, and run locally. ## Gallery @@ -33,7 +33,7 @@ Ready-to-run examples for common thumbnail and social card formats. Each recipe ## Prerequisites -All recipes assume QuickThumb is installed: +All recipes assume quickthumb is installed: ```bash pip install quickthumb @@ -47,4 +47,4 @@ pip install "quickthumb[rembg]" ## Asset paths -The code snippets below use placeholder paths like `"background.jpg"` and `"portrait.png"`. Swap these for your own local files or remote URLs — QuickThumb accepts both. +The code snippets below use placeholder paths like `"background.jpg"` and `"portrait.png"`. Swap these for your own local files or remote URLs — quickthumb accepts both. diff --git a/docs/cookbook/instagram-card.md b/docs/cookbook/instagram-card.md index a9a5d43..e5f443a 100644 --- a/docs/cookbook/instagram-card.md +++ b/docs/cookbook/instagram-card.md @@ -1,5 +1,5 @@ --- -description: Build a square Instagram news card with QuickThumb using a full-bleed image, gradient overlay, badge, headline, and metadata. +description: Build a square Instagram news card with quickthumb using a full-bleed image, gradient overlay, badge, headline, and metadata. --- # Instagram Card diff --git a/docs/cookbook/podcast-promo.md b/docs/cookbook/podcast-promo.md index 4b2b1ce..9625299 100644 --- a/docs/cookbook/podcast-promo.md +++ b/docs/cookbook/podcast-promo.md @@ -1,5 +1,5 @@ --- -description: Build a podcast promo card with QuickThumb using remote images, webfonts, background removal, rich text, and layered composition. +description: Build a podcast promo card with quickthumb using remote images, webfonts, background removal, rich text, and layered composition. --- # Podcast Promo @@ -168,7 +168,7 @@ SHOW_FONT_URL = ( ### Remote images -Both the background and the guest portrait are fetched from remote URLs. QuickThumb downloads them at render time and caches them locally. Pass any `http://` or `https://` URL to `image=` or `path=`. +Both the background and the guest portrait are fetched from remote URLs. quickthumb downloads them at render time and caches them locally. Pass any `http://` or `https://` URL to `image=` or `path=`. ### Webfont from URL diff --git a/docs/cookbook/shorts-cover.md b/docs/cookbook/shorts-cover.md index aeb3215..33c2740 100644 --- a/docs/cookbook/shorts-cover.md +++ b/docs/cookbook/shorts-cover.md @@ -1,5 +1,5 @@ --- -description: Render a vertical shorts cover from a QuickThumb JSON spec, demonstrating a JSON-first workflow for mobile video thumbnails. +description: Render a vertical shorts cover from a quickthumb JSON spec, demonstrating a JSON-first workflow for mobile video thumbnails. --- # Shorts / Vertical Cover diff --git a/docs/cookbook/webfonts-rembg.md b/docs/cookbook/webfonts-rembg.md index 9103f6d..52cd6cc 100644 --- a/docs/cookbook/webfonts-rembg.md +++ b/docs/cookbook/webfonts-rembg.md @@ -1,5 +1,5 @@ --- -description: Use QuickThumb webfonts and background removal to load remote typefaces, isolate subjects, and compose polished thumbnail images. +description: Use quickthumb webfonts and background removal to load remote typefaces, isolate subjects, and compose polished thumbnail images. --- # Webfonts & Background Removal @@ -10,7 +10,7 @@ Two features that require a bit of setup but unlock a lot of creative flexibilit ## Webfonts -QuickThumb can load fonts from remote URLs at render time. The font file is downloaded once and cached locally. +quickthumb can load fonts from remote URLs at render time. The font file is downloaded once and cached locally. ### Basic usage @@ -83,7 +83,7 @@ os.environ["QUICKTHUMB_DEFAULT_FONT"] = "Roboto" canvas.text(content="Hello", font="Roboto", size=64, color="#FFFFFF", align="center") ``` -QuickThumb searches `QUICKTHUMB_FONT_DIR` for files matching the family name, and falls back to `QUICKTHUMB_DEFAULT_FONT` when `font` is omitted. +quickthumb searches `QUICKTHUMB_FONT_DIR` for files matching the family name, and falls back to `QUICKTHUMB_DEFAULT_FONT` when `font` is omitted. --- diff --git a/docs/cookbook/youtube-thumbnail.md b/docs/cookbook/youtube-thumbnail.md index 23fe0b0..4f70471 100644 --- a/docs/cookbook/youtube-thumbnail.md +++ b/docs/cookbook/youtube-thumbnail.md @@ -1,5 +1,5 @@ --- -description: Build YouTube thumbnails with QuickThumb using darkened photos, bold text, outlines, reaction layouts, and tutorial compositions. +description: Build YouTube thumbnails with quickthumb using darkened photos, bold text, outlines, reaction layouts, and tutorial compositions. --- # YouTube Thumbnails @@ -33,7 +33,7 @@ from quickthumb import Canvas, Filter, Stroke, TextPart effects=[Stroke(width=8, color="#000000")], ), TextPart( - text="Try QuickThumb Today", + text="Try quickthumb Today", color="#E0E0E0", size=48, effects=[Stroke(width=4, color="#000000")], @@ -347,6 +347,6 @@ canvas.render("youtube_tutorial.png") **Key techniques:** -- `add_step()` helper function keeps repeated badge+label patterns DRY while still using the standard QuickThumb API +- `add_step()` helper function keeps repeated badge+label patterns DRY while still using the standard quickthumb API - Two gradient backgrounds create a blue glow on the left edge without a visible boundary - The `"#3B82F630"` color (with `30` alpha) renders the divider as a subtle translucent line diff --git a/docs/faq.md b/docs/faq.md index 651cab3..e7b259e 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -1,5 +1,5 @@ --- -description: Answers to common QuickThumb questions about installation, fonts, images, rendering behavior, JSON specs, and supported Python versions. +description: Answers to common quickthumb questions about installation, fonts, images, rendering behavior, JSON specs, and supported Python versions. --- # FAQ @@ -93,7 +93,7 @@ canvas.text( ) ``` -QuickThumb downloads and caches the font file. Note: when using a webfont URL, `bold`, `italic`, and `weight` are ignored — download separate URLs for bold/italic variants. +quickthumb downloads and caches the font file. Note: when using a webfont URL, `bold`, `italic`, and `weight` are ignored — download separate URLs for bold/italic variants. --- @@ -101,7 +101,7 @@ QuickThumb downloads and caches the font file. Note: when using a webfont URL, ` ### Can I use remote images? -Yes, both `canvas.background(image=...)` and `canvas.image(path=...)` accept `http://` and `https://` URLs. QuickThumb downloads and caches them during rendering. +Yes, both `canvas.background(image=...)` and `canvas.image(path=...)` accept `http://` and `https://` URLs. quickthumb downloads and caches them during rendering. ### My image has a white background I want to remove. How? diff --git a/docs/favicon.ico b/docs/favicon.ico new file mode 100644 index 0000000..3b14fd7 Binary files /dev/null and b/docs/favicon.ico differ diff --git a/docs/getting-started.md b/docs/getting-started.md index f89e7e2..dd82eb1 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,5 +1,5 @@ --- -description: Build your first QuickThumb image by creating a canvas, layering backgrounds, text, shapes, and images, then exporting the rendered thumbnail. +description: Build your first quickthumb image by creating a canvas, layering backgrounds, text, shapes, and images, then exporting the rendered thumbnail. --- # Getting Started @@ -173,7 +173,7 @@ data_url = canvas.to_data_url(format="JPEG", quality=90) ## JSON workflow -You can also drive QuickThumb from a JSON config instead of Python code: +You can also drive quickthumb from a JSON config instead of Python code: ```python from quickthumb import Canvas @@ -186,7 +186,7 @@ config = """ { "type": "background", "color": "#111827" }, { "type": "text", - "content": "Hello QuickThumb", + "content": "Hello quickthumb", "size": 72, "color": "#FFFFFF", "align": "center", diff --git a/docs/index.md b/docs/index.md index 5dae122..9bbf412 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,9 +1,9 @@ --- -title: QuickThumb -description: Create thumbnails and social images with QuickThumb's layered Python API, JSON specs, remote images, webfonts, effects, and deterministic rendering. +title: quickthumb +description: Create thumbnails and social images with quickthumb's layered Python API, JSON specs, remote images, webfonts, effects, and deterministic rendering. --- -# QuickThumb +# quickthumb **Programmatic thumbnail and social image generation** — layered Python and JSON APIs built for speed, AI workflows, and creative control. @@ -53,7 +53,7 @@ canvas = ( canvas.render("thumbnail.png") ``` -## Why QuickThumb +## Why quickthumb - Layer-based composition — backgrounds, text, images, shapes stack in call order - Works with Python method chaining **and** JSON specs — same result either way diff --git a/docs/installation.md b/docs/installation.md index 907164d..e93b5f3 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -1,5 +1,5 @@ --- -description: Install QuickThumb with pip or uv, enable optional background removal support, and configure font-related environment variables. +description: Install quickthumb with pip or uv, enable optional background removal support, and configure font-related environment variables. --- # Installation @@ -34,7 +34,7 @@ uv pip install "quickthumb[rembg]" ``` !!! note - The `rembg` extra pulls in `onnxruntime` and will download a model (~170 MB) on first use. It is not required for any other QuickThumb feature. + The `rembg` extra pulls in `onnxruntime` and will download a model (~170 MB) on first use. It is not required for any other quickthumb feature. ## Verify the installation @@ -45,7 +45,7 @@ print(quickthumb.__version__) ## Environment Variables -QuickThumb reads two optional environment variables at startup: +quickthumb reads two optional environment variables at startup: | Variable | Purpose | | --- | --- | diff --git a/docs/json-schema.md b/docs/json-schema.md index 7639851..f366d67 100644 --- a/docs/json-schema.md +++ b/docs/json-schema.md @@ -1,10 +1,10 @@ --- -description: Use QuickThumb JSON specs with AI workflows to generate, validate, store, and render thumbnail designs from structured data. +description: Use quickthumb JSON specs with AI workflows to generate, validate, store, and render thumbnail designs from structured data. --- # JSON Schema & AI Workflow -QuickThumb canvases can be fully described as JSON. This makes them easy to generate with an LLM, store in a database, pass through an API, or version-control alongside your content. +quickthumb canvases can be fully described as JSON. This makes them easy to generate with an LLM, store in a database, pass through an API, or version-control alongside your content. ## Round-trip serialization @@ -26,7 +26,7 @@ canvas = Canvas.from_json(json_str) ## JSON structure -A QuickThumb JSON document has three top-level fields: +A quickthumb JSON document has three top-level fields: ```json { @@ -350,12 +350,12 @@ A full YouTube-style thumbnail spec: ## AI workflow -QuickThumb JSON is well-suited for LLM generation because the schema is flat, every field is typed, and the output is directly renderable without transformation. +quickthumb JSON is well-suited for LLM generation because the schema is flat, every field is typed, and the output is directly renderable without transformation. ### Recommended prompt (JSON output) ```text -Generate a QuickThumb JSON config for a 1280×720 YouTube thumbnail. +Generate a quickthumb JSON config for a 1280×720 YouTube thumbnail. Rules: - Top-level fields: "width", "height", "layers" @@ -373,7 +373,7 @@ Return only the JSON object, no explanation. ### Recommended prompt (Python output) ```text -Generate QuickThumb Python code for a 1280×720 YouTube thumbnail. +Generate quickthumb Python code for a 1280×720 YouTube thumbnail. Available imports: from quickthumb import Canvas, Filter, LinearGradient, RadialGradient, Background, @@ -390,7 +390,7 @@ Return only the Python code block. ### Validation and iteration workflow -1. Have the model produce a QuickThumb JSON or Python spec. +1. Have the model produce a quickthumb JSON or Python spec. 2. Render it locally with `canvas.render("preview.png")`. 3. Identify what to change — colors, text, layout — without rewriting the full spec. 4. Feed the rendered result back to the model with targeted instructions if needed. diff --git a/docs/site.webmanifest b/docs/site.webmanifest new file mode 100644 index 0000000..830da8f --- /dev/null +++ b/docs/site.webmanifest @@ -0,0 +1,19 @@ +{ + "name": "quickthumb", + "short_name": "quickthumb", + "icons": [ + { + "src": "assets/brand/quickthumb-icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "assets/brand/quickthumb-icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#4d89fb", + "background_color": "#ffffff", + "display": "standalone" +} diff --git a/docs/stylesheets/extra.css b/docs/stylesheets/extra.css index 2f8059c..8cf951b 100644 --- a/docs/stylesheets/extra.css +++ b/docs/stylesheets/extra.css @@ -1,83 +1,452 @@ -/* ─── Brand colors ─────────────────────────────────────────────────────────── */ +/* ─── Brand palette ────────────────────────────────────────────────────────── */ :root { - --qt-cyan: #22d3ee; - --qt-black: #0d0d0d; - --qt-surface: #111111; + --qt-blue-300: #a9c6ff; + --qt-blue-400: #7fadff; + --qt-blue-500: #4d89fb; + --qt-blue-600: #2f6fe8; + --qt-blue-700: #1f56c5; + --qt-blue-800: #173f93; - --md-primary-fg-color: var(--qt-black); - --md-primary-fg-color--light: #1a1a1a; - --md-primary-fg-color--dark: #000000; - --md-primary-bg-color: #ffffff; + --qt-ice-50: #fbfdff; + --qt-ice-100: #f2f7ff; + --qt-ice-200: #e4eeff; - --md-accent-fg-color: var(--qt-cyan); - --md-accent-fg-color--transparent: #22d3ee1a; + --qt-ink-900: #0a1730; + --qt-ink-800: #102142; + --qt-ink-700: #162b57; + --qt-ink-100: #dbe7ff; } +/* ─── Light scheme tokens ─────────────────────────────────────────────────── */ +[data-md-color-scheme="default"] { + --md-default-fg-color: #1a395d; + --md-default-fg-color--light: #537090; + --md-default-fg-color--lighter: #89a0bc; + --md-default-fg-color--lightest: rgba(77, 137, 251, 0.16); + + --md-default-bg-color: var(--qt-ice-50); + --md-default-bg-color--light: #ffffff; + --md-default-bg-color--lighter: var(--qt-ice-100); + --md-default-bg-color--lightest: var(--qt-ice-200); + + --md-primary-fg-color: var(--qt-blue-700); + --md-primary-fg-color--light: var(--qt-blue-500); + --md-primary-fg-color--dark: var(--qt-blue-800); + --md-primary-bg-color: #ffffff; + + --md-accent-fg-color: var(--qt-blue-600); + --md-accent-fg-color--transparent: #4d89fb1a; + + --md-typeset-a-color: var(--qt-blue-600); + --md-code-fg-color: #1b3150; + --md-code-bg-color: #eef4ff; + + --qt-drawer-title-fg: rgba(255, 255, 255, 0.96); + --qt-drawer-title-bg: linear-gradient( + 145deg, + #1b4b97 0%, + #245fc6 54%, + #2f6fe8 100% + ); + --qt-drawer-source-fg: rgba(255, 255, 255, 0.94); + --qt-drawer-source-bg: linear-gradient( + 145deg, + #14346f 0%, + #184896 52%, + #1f56b8 100% + ); + --qt-drawer-edge: rgba(255, 255, 255, 0.14); + --qt-drawer-list-shadow: rgba(31, 86, 184, 0.14); +} + +/* ─── Dark scheme tokens ──────────────────────────────────────────────────── */ [data-md-color-scheme="slate"] { - --md-default-bg-color: #0f0f0f; - --md-default-bg-color--light: #161616; - --md-default-bg-color--lighter: #1c1c1c; - --md-default-bg-color--lightest: #242424; + --md-default-fg-color: #eaf2ff; + --md-default-fg-color--light: #c8d9f2; + --md-default-fg-color--lighter: #91abd0; + --md-default-fg-color--lightest: rgba(127, 195, 255, 0.18); + + --md-default-bg-color: #09111f; + --md-default-bg-color--light: #0e172b; + --md-default-bg-color--lighter: #13203a; + --md-default-bg-color--lightest: #1a2a49; + + --md-primary-fg-color: #4f97ff; + --md-primary-fg-color--light: #7fc3ff; + --md-primary-fg-color--dark: #2b63d0; + --md-primary-bg-color: #f8fbff; - --md-code-bg-color: #161616; - --md-code-fg-color: #e2e8f0; + --md-accent-fg-color: #8ed8ff; + --md-accent-fg-color--transparent: #8ed8ff24; + + --md-typeset-a-color: #9edfff; + --md-code-fg-color: var(--qt-ink-100); + --md-code-bg-color: #101b33; + + --qt-drawer-title-fg: rgba(232, 240, 255, 0.94); + --qt-drawer-title-bg: linear-gradient( + 145deg, + #14346f 0%, + #1b4b97 52%, + #225ab1 100% + ); + --qt-drawer-source-fg: rgba(232, 240, 255, 0.94); + --qt-drawer-source-bg: linear-gradient( + 145deg, + #102b5d 0%, + #163f82 52%, + #1b4c96 100% + ); + --qt-drawer-edge: rgba(127, 195, 255, 0.16); + --qt-drawer-list-shadow: rgba(127, 195, 255, 0.16); } -/* ─── Header ───────────────────────────────────────────────────────────────── */ +/* ─── Header and tabs ─────────────────────────────────────────────────────── */ .md-header { - background-color: var(--qt-black); - box-shadow: 0 1px 0 0 #ffffff14; + background: + radial-gradient( + circle at 18% -14%, + rgba(255, 255, 255, 0.1), + rgba(255, 255, 255, 0) 24% + ), + radial-gradient( + circle at 72% 0%, + rgba(143, 216, 255, 0.14), + rgba(143, 216, 255, 0) 24% + ), + linear-gradient(118deg, #2258bc 0%, #2b6fd4 46%, #277dca 100%); +} + +.md-tabs { + background: + linear-gradient( + 90deg, + rgba(143, 216, 255, 0.06), + rgba(143, 216, 255, 0) 24% + ), + linear-gradient(118deg, #245fbf 0%, #2b6fd4 48%, #2a7bc9 100%); + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08); +} + +[data-md-color-scheme="slate"] .md-header { + background: + radial-gradient( + circle at 20% -16%, + rgba(127, 195, 255, 0.12), + rgba(127, 195, 255, 0) 24% + ), + radial-gradient( + circle at 78% 0%, + rgba(78, 151, 255, 0.14), + rgba(78, 151, 255, 0) 24% + ), + linear-gradient(118deg, #14346f 0%, #1b4b97 48%, #225ab1 100%); +} + +[data-md-color-scheme="slate"] .md-tabs { + background: + linear-gradient( + 90deg, + rgba(127, 195, 255, 0.08), + rgba(127, 195, 255, 0) 22% + ), + linear-gradient(118deg, #163877 0%, #1c4b97 48%, #1d58a5 100%); + box-shadow: inset 0 1px 0 rgba(127, 195, 255, 0.1); } .md-header__title { - font-weight: 700; - letter-spacing: -0.02em; + margin-left: 0; + min-width: 0; + font-weight: 700; + letter-spacing: -0.02em; +} + +.md-header__button, +.md-header__title-link, +.md-header__topic, +.md-header__source { + color: rgba(255, 255, 255, 0.98); +} + +.md-header__topic[data-md-component="header-topic"] { + color: rgba(255, 255, 255, 0.88); +} + +[data-md-color-scheme="default"] + .md-header__topic[data-md-component="header-topic"] { + color: rgba(255, 255, 255, 0.96); +} + +.md-header__title-link { + color: inherit; + display: flex; + flex: 1 1 auto; + margin-right: auto; + min-width: 0; + margin-left: 0; + padding: 0; + text-decoration: none; +} + +[dir="ltr"] .md-header__title.md-header__title-link { + margin-left: 0; +} + +[dir="rtl"] .md-header__title.md-header__title-link { + margin-right: 0; +} + +.md-header__title-link .md-header__ellipsis { + min-width: 0; +} + +.md-header__button.md-logo { + flex-shrink: 0; + margin-right: 0.1rem; +} + +.md-header__button.md-logo img, +.md-header__button.md-logo svg { + height: 2.05rem; + width: 2.05rem; +} + +.md-header__button:hover, +.md-header__title-link:hover, +.md-header__source .md-source:hover { + opacity: 1; + background: none; + box-shadow: none; +} + +.md-header__button:focus-visible, +.md-header__title-link:focus-visible, +.md-header__source .md-source:focus-visible { + outline: 0.12rem solid rgba(255, 255, 255, 0.28); + outline-offset: 0; +} + +[data-md-color-scheme="default"] .md-header__button:focus-visible, +[data-md-color-scheme="default"] .md-header__title-link:focus-visible, +[data-md-color-scheme="default"] .md-header__source .md-source:focus-visible { + outline-color: rgba(10, 23, 48, 0.48); +} + +.md-tabs__link { + color: rgba(255, 255, 255, 0.88); + margin-top: 0; + padding: 0.7rem 0.78rem 0.62rem; + border-radius: 0; + transition: + color 125ms, + opacity 125ms, + box-shadow 125ms; +} + +.md-tabs__list { + gap: 0.65rem; +} + +.md-tabs__item { + height: auto; +} + +[data-md-color-scheme="slate"] .md-tabs__link:is(:focus, :hover) { + background-color: transparent; + box-shadow: inset 0 -0.12rem 0 rgba(127, 195, 255, 0.3); +} + +[data-md-color-scheme="slate"] .md-tabs__link--active { + background-color: transparent; + box-shadow: inset 0 -0.14rem 0 rgba(127, 195, 255, 0.75); +} + +[data-md-color-scheme="default"] .md-tabs__link:is(:focus, :hover) { + background-color: transparent; + box-shadow: inset 0 -0.12rem 0 rgba(255, 255, 255, 0.34); +} + +[data-md-color-scheme="default"] .md-tabs__link--active { + background-color: transparent; + box-shadow: inset 0 -0.14rem 0 rgba(255, 255, 255, 0.82); } -/* ─── Sidebar ──────────────────────────────────────────────────────────────── */ +/* ─── Sidebar and navigation ──────────────────────────────────────────────── */ [data-md-color-scheme="slate"] .md-sidebar { - background-color: var(--md-default-bg-color); + background-color: var(--md-default-bg-color); } -/* ─── Navigation active item ───────────────────────────────────────────────── */ -.md-nav__link--active, -.md-nav__link:is(:focus, :hover) { - color: var(--qt-cyan) !important; +[data-md-color-scheme="default"] .md-nav--primary .md-nav__link { + color: var(--md-default-fg-color--light); } -/* ─── Tabs ─────────────────────────────────────────────────────────────────── */ -.md-tabs { - background-color: var(--qt-black); +[data-md-color-scheme="slate"] .md-nav--primary .md-nav__link { + color: var(--md-default-fg-color--light); +} + +[data-md-color-scheme="default"] + .md-nav--primary + .md-nav__item + .md-nav + .md-nav__link { + color: #506987; +} + +[data-md-color-scheme="slate"] + .md-nav--primary + .md-nav__item + .md-nav + .md-nav__link { + color: #8aa6d8; +} + +[data-md-color-scheme="default"] + .md-nav--primary + .md-nav__item--nested + > .md-nav__link { + color: #17304d; + font-weight: 600; +} + +[data-md-color-scheme="slate"] + .md-nav--primary + .md-nav__item--nested + > .md-nav__link { + color: #e8f0ff; + font-weight: 600; +} + +.md-nav__link--active, +.md-nav__link:is(:focus, :hover) { + color: var(--md-accent-fg-color) !important; } .md-tabs__link--active, .md-tabs__link:is(:focus, :hover) { - color: var(--qt-cyan) !important; + color: #ffffff !important; } -/* ─── Code blocks ──────────────────────────────────────────────────────────── */ +.md-nav--primary .md-nav__item .md-nav { + border-left: 0; +} + +@media screen and (max-width: 76.234375em) { + .md-nav--primary > .md-nav__list > .md-nav__item { + border-top: 0; + } + + .md-nav--primary > .md-nav__title, + .md-nav--primary > .md-nav__source { + background: + radial-gradient( + circle at 84% 0%, + rgba(255, 255, 255, 0.05), + rgba(255, 255, 255, 0) 24% + ), + var(--qt-drawer-title-bg); + color: var(--qt-drawer-title-fg); + } + + .md-nav--primary > .md-nav__source { + color: var(--qt-drawer-source-fg); + } + + .md-nav--primary > .md-nav__title .md-nav__icon, + .md-nav--primary > .md-nav__source .md-source__icon { + color: inherit; + } + + .md-nav--primary > .md-nav__title .md-ellipsis, + .md-nav--primary > .md-nav__source .md-source, + .md-nav--primary > .md-nav__source .md-source__repository, + .md-nav--primary > .md-nav__source .md-source__facts { + color: inherit; + } + + .md-nav--primary > .md-nav__source .md-source { + padding-block: 0.25rem; + } + + .md-nav--primary > .md-nav__source .md-source__repository { + font-weight: 600; + } + + .md-nav--primary > .md-nav__source .md-source__facts { + opacity: 0.96; + } + + .md-nav--primary > .md-nav__title ~ .md-nav__list { + box-shadow: 0 0.05rem 0 var(--qt-drawer-list-shadow) inset; + } +} + +@media screen and (max-width: 44.984375em) { + .md-header__button.md-logo { + display: none; + } + + .md-header__title-link { + padding-left: 0; + } + + .md-header__title-link .md-header__topic[data-md-component="header-topic"] { + display: none; + } + + .md-header__title-link .md-header__topic:first-child, + .md-header__title--active .md-header__topic:first-child { + opacity: 1; + pointer-events: auto; + position: static; + transform: none; + z-index: 0; + } + + .md-typeset p:has(> .md-button) { + flex-direction: column; + align-items: stretch; + } + + .md-typeset p:has(> .md-button) > .md-button { + text-align: center; + } +} + +/* ─── Code blocks ─────────────────────────────────────────────────────────── */ [data-md-color-scheme="slate"] .highlight pre, -[data-md-color-scheme="slate"] code { - border-radius: 6px; +[data-md-color-scheme="slate"] code, +[data-md-color-scheme="default"] .highlight pre, +[data-md-color-scheme="default"] code { + border-radius: 6px; } -/* ─── Typography ───────────────────────────────────────────────────────────── */ +/* ─── Typography ──────────────────────────────────────────────────────────── */ .md-typeset h1 { - font-weight: 800; - letter-spacing: -0.03em; + font-weight: 800; + letter-spacing: -0.03em; } .md-typeset h2 { - font-weight: 700; - letter-spacing: -0.02em; + font-weight: 700; + letter-spacing: -0.02em; } .md-typeset h3 { - font-weight: 600; + font-weight: 600; +} + +.md-typeset p:has(> .md-button) { + display: flex; + flex-wrap: wrap; + gap: 0.8rem; +} + +.md-typeset p:has(> .md-button) > .md-button { + margin: 0; } -/* ─── Admonitions ──────────────────────────────────────────────────────────── */ +/* ─── Admonitions ─────────────────────────────────────────────────────────── */ .md-typeset .admonition, .md-typeset details { - border-radius: 6px; + border-radius: 6px; } diff --git a/examples/README.md b/examples/README.md index 0e1e4f1..825f2a3 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,6 +1,6 @@ -# QuickThumb Examples +# quickthumb Examples -This directory contains runnable end-to-end compositions that match the current QuickThumb API. +This directory contains runnable end-to-end compositions that match the current quickthumb API. ## Run an Example @@ -114,7 +114,7 @@ Shows: - `remove_background=True` on the portrait layer for a cutout-style guest visual - Layered promo-card styling with shapes, shadows, and heavy headline typography -Use it when you want an end-to-end podcast or interview promo example that exercises QuickThumb's network-backed asset loading and portrait cutout workflow. +Use it when you want an end-to-end podcast or interview promo example that exercises quickthumb's network-backed asset loading and portrait cutout workflow. ### `shorts_cover_agent.py` diff --git a/examples/shorts_cover_agent.py b/examples/shorts_cover_agent.py index 60ebb89..0d6961a 100644 --- a/examples/shorts_cover_agent.py +++ b/examples/shorts_cover_agent.py @@ -1,7 +1,7 @@ """ JSON-first vertical cover example. -This script simulates the workflow where an AI agent emits a QuickThumb JSON spec +This script simulates the workflow where an AI agent emits a quickthumb JSON spec and the application renders it with `Canvas.from_json(...)`. """ diff --git a/mkdocs.yml b/mkdocs.yml index b7ec0b7..9fbff02 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,4 +1,4 @@ -site_name: QuickThumb +site_name: quickthumb site_description: Programmatic thumbnail and social image generation with layered Python and JSON APIs site_url: https://sjquant.github.io/quickthumb site_author: Seonu Jang @@ -11,6 +11,8 @@ site_dir: site theme: name: material custom_dir: docs/overrides + logo: assets/brand/quickthumb-icon.png + favicon: assets/brand/favicon.png palette: - media: "(prefers-color-scheme: light)" scheme: default @@ -47,7 +49,7 @@ extra_css: - stylesheets/extra.css extra: - social_image_url: https://sjquant.github.io/quickthumb/assets/examples/youtube_thumbnail_01.png + social_image_url: https://sjquant.github.io/quickthumb/assets/brand/social-preview.png markdown_extensions: - admonition diff --git a/overrides/main.html b/overrides/main.html new file mode 100644 index 0000000..e22be11 --- /dev/null +++ b/overrides/main.html @@ -0,0 +1,9 @@ +{% extends "base.html" %} + +{% block extrahead %} + {{ super() }} + + + + +{% endblock %} diff --git a/overrides/partials/header.html b/overrides/partials/header.html new file mode 100644 index 0000000..0f2f22d --- /dev/null +++ b/overrides/partials/header.html @@ -0,0 +1,80 @@ +{#- + This file was automatically generated - do not edit +-#} +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--shadow md-header--lifted" %} +{% elif "navigation.tabs" not in features %} + {% set class = class ~ " md-header--shadow" %} +{% endif %} +
+ + {% if "navigation.tabs.sticky" in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} +
diff --git a/quickthumb/cli.py b/quickthumb/cli.py index acf0b10..d120a0a 100644 --- a/quickthumb/cli.py +++ b/quickthumb/cli.py @@ -12,12 +12,12 @@ _VALID_FORMATS = {"PNG", "JPEG", "WEBP"} -app = typer.Typer(help="QuickThumb — programmatic thumbnail generation") +app = typer.Typer(help="quickthumb — programmatic thumbnail generation") @app.callback() def _callback() -> None: - """QuickThumb CLI.""" + """quickthumb CLI.""" def main() -> None: diff --git a/quickthumb/models.py b/quickthumb/models.py index 6c41633..5bb8652 100644 --- a/quickthumb/models.py +++ b/quickthumb/models.py @@ -144,7 +144,7 @@ def _validate_align_with_hv_tuple(v: Any) -> Align | None: AlignWithHVTuple = Annotated[Align | None, BeforeValidator(_validate_align_with_hv_tuple)] -class QuickThumbModel(BaseModel): +class quickthumbModel(BaseModel): @model_validator(mode="wrap") @classmethod def handle_pydantic_error(cls, data: Any, handler): @@ -161,19 +161,22 @@ def handle_pydantic_error(cls, data: Any, handler): raise ValidationError(formatted_msg, original_error=e) from e -class LinearGradient(QuickThumbModel): +QuickThumbModel = quickthumbModel + + +class LinearGradient(quickthumbModel): type: Literal["linear"] = "linear" angle: float stops: list[tuple[str, float]] -class RadialGradient(QuickThumbModel): +class RadialGradient(quickthumbModel): type: Literal["radial"] = "radial" stops: list[tuple[str, float]] center: tuple[float, float] = (0.5, 0.5) -class TextFillImage(QuickThumbModel): +class TextFillImage(quickthumbModel): type: Literal["image"] = "image" path: str fit: Annotated[ @@ -184,13 +187,13 @@ class TextFillImage(QuickThumbModel): TextFill = Annotated[LinearGradient | RadialGradient | TextFillImage, Discriminator("type")] -class Stroke(QuickThumbModel): +class Stroke(quickthumbModel): type: Literal["stroke"] = "stroke" width: PositiveInt color: HexColor -class Shadow(QuickThumbModel): +class Shadow(quickthumbModel): type: Literal["shadow"] = "shadow" offset_x: int offset_y: int @@ -205,14 +208,14 @@ def validate_blur_radius(cls, v: int) -> int: return v -class Glow(QuickThumbModel): +class Glow(quickthumbModel): type: Literal["glow"] = "glow" color: HexColor radius: PositiveInt opacity: OpacityField = 1.0 -class Background(QuickThumbModel): +class Background(quickthumbModel): type: Literal["background"] = "background" color: HexColor padding: int | tuple[int, int] | tuple[int, int, int, int] = 0 @@ -246,7 +249,7 @@ def validate_border_radius(cls, v: int) -> int: return v -class Filter(QuickThumbModel): +class Filter(quickthumbModel): type: Literal["filter"] = "filter" blur: NonNegativeInt = 0 brightness: PositiveFloat = 1.0 @@ -264,7 +267,7 @@ def validate_saturation(cls, v: float) -> float: _GRAIN_BLEND_MODES = frozenset({"overlay", "screen", "multiply", "normal"}) -class Grain(QuickThumbModel): +class Grain(quickthumbModel): type: Literal["grain"] = "grain" intensity: float monochrome: bool = True @@ -296,7 +299,7 @@ def validate_blend_mode(cls, v: str) -> str: BackgroundEffect = Annotated[Filter | Grain, Discriminator("type")] -class TextPart(QuickThumbModel): +class TextPart(quickthumbModel): text: str color: HexColor | None = None fill: TextFill | None = None @@ -323,7 +326,7 @@ def validate_text(cls, v: str) -> str: return v -class BackgroundLayer(QuickThumbModel): +class BackgroundLayer(quickthumbModel): type: Literal["background"] color: HexColor | tuple | None = None gradient: Annotated[LinearGradient | RadialGradient, Discriminator("type")] | None = None @@ -357,7 +360,7 @@ def serialize_color(self, v: str | tuple | None) -> str | None: return "#" + "".join(f"{c:02X}" for c in v) -class TextLayer(QuickThumbModel): +class TextLayer(quickthumbModel): type: Literal["text"] content: str | list[TextPart] font: str | None = None @@ -442,7 +445,7 @@ def serialize_align(self, align: Align | None) -> str | None: return align.value -class OutlineLayer(QuickThumbModel): +class OutlineLayer(quickthumbModel): type: Literal["outline"] width: PositiveInt color: HexColor @@ -450,7 +453,7 @@ class OutlineLayer(QuickThumbModel): opacity: OpacityField = 1.0 -class ImageLayer(QuickThumbModel): +class ImageLayer(quickthumbModel): type: Literal["image"] path: str position: tuple @@ -493,7 +496,7 @@ def serialize_align(self, align: Align) -> str: return align.value -class ShapeLayer(QuickThumbModel): +class ShapeLayer(quickthumbModel): type: Literal["shape"] shape: Literal["rectangle", "ellipse"] position: tuple @@ -537,7 +540,7 @@ def serialize_align(self, align: Align | None) -> str | None: ] -class CanvasModel(QuickThumbModel): +class CanvasModel(quickthumbModel): width: PositiveInt height: PositiveInt layers: list[LayerType] diff --git a/specs/SPEC.md b/specs/SPEC.md deleted file mode 100644 index fd28bd1..0000000 --- a/specs/SPEC.md +++ /dev/null @@ -1,499 +0,0 @@ -# QuickThumb Feature Spec - -This document specifies planned and exploratory features for QuickThumb. It is a forward-looking companion to README.md, which documents the **currently implemented** API. - -### Status Legend - -| Status | Meaning | -| ------------- | --------------------------------------------- | -| `planned` | Committed for implementation; not yet shipped | -| `in progress` | Actively being developed | -| `done` | Shipped; refer to README.md for the final API | -| `exploratory` | Under consideration; design not committed | - -### Feature Status - -| # | Feature | Status | -| --- | ---------------------------- | ------------- | -| 1 | CLI (`quickthumb` command) | `done` | -| 2 | Template System | `done` | -| 3 | Gradient / Image-Filled Text | `done` | -| 4 | Noise / Grain Effect | `done` | -| 5 | Presentation & Video | `exploratory` | - ---- - -## 1. CLI — `done` - -A `quickthumb` command-line tool for rendering JSON specs without writing Python. - -### Installation - -The CLI is an optional extra to avoid pulling `typer` into the core dependency set. - -```bash -uv pip install "quickthumb[cli]" -``` - -`pyproject.toml` entry point: - -```toml -[project.scripts] -quickthumb = "quickthumb.cli:main" -``` - -### Subcommands - -#### `render` - -Render a JSON spec file to an image. - -```bash -quickthumb render spec.json -quickthumb render spec.json -o thumbnail.png -quickthumb render spec.json -o output.webp --format WEBP --quality 85 -``` - -Template variable substitution via `--var`: - -```bash -quickthumb render template.json \ - --var title="10 Python Tips" \ - --var image=photo.png \ - -o out.png -``` - -Parameters: - -- `spec`: required; path to a JSON spec file -- `-o` / `--output`: output file path; defaults to `output.png` in the current directory -- `--format`: `PNG`, `JPEG`, or `WEBP`; inferred from output extension when omitted -- `--quality`: integer quality for `JPEG` and `WEBP` -- `--var KEY=VALUE`: substitutes `$KEY` / `${KEY}` placeholders in the spec before parsing; repeatable - -#### `watch` (stretch goal) - -Re-render automatically when the spec file changes. Requires `watchfiles`. - -```bash -quickthumb watch spec.json -o thumbnail.png -``` - -Install with: - -```bash -uv pip install "quickthumb[cli]" -``` - -### Pipeline - -1. Read the JSON spec file from disk. -2. Substitute `--var` placeholders (string-level, before JSON parsing). -3. Call `Canvas.from_json()` on the substituted string. -4. Call `canvas.render(output, format=..., quality=...)`. - -### Exit Codes - -| Code | Meaning | -| ---- | ----------------------------------------------------------------------- | -| 0 | Success | -| 1 | Validation error (bad spec, missing required field, unknown layer type) | -| 2 | Rendering error (missing file, download failure, unsupported format) | - -### Notes - -- `typer` is only imported inside `quickthumb/cli.py`; the rest of the library does not depend on it. -- `quickthumb watch` exits with code 1 if `watchfiles` is not installed. -- Errors print to stderr; the rendered image path prints to stdout on success. - ---- - -## 2. Template System — `done` - -Reusable JSON specs with variable placeholders. Useful for batch generation and AI-driven workflows. - -### Placeholder Syntax - -Templates use `$variable` and `${variable}` placeholders anywhere in the JSON text. Substitution happens at the string level before the JSON is parsed. - -```json -{ - "width": 1280, - "height": 720, - "layers": [ - { "type": "background", "color": "${bg_color}" }, - { - "type": "text", - "content": "$title", - "size": 88, - "color": "#FFFFFF", - "position": ["8%", "50%"] - }, - { - "type": "image", - "path": "${subject_image}", - "position": ["74%", "54%"], - "width": 420, - "height": 520, - "align": ["center", "middle"] - } - ] -} -``` - -### Python API - -```python -from quickthumb import Canvas - -# From a template file path -canvas = Canvas.from_template( - "templates/youtube-16x9.json", - variables={ - "title": "10 Python Tips", - "bg_color": "#0F172A", - "subject_image": "portrait.png", - }, -) -canvas.render("thumbnail.png") - -# From a template string -spec = open("template.json").read() -canvas = Canvas.from_template(spec, variables={"title": "Hello"}) -``` - -Parameters: - -- `spec_or_path`: required; a JSON string or a file path to a `.json` template -- `variables`: dict mapping placeholder names to string values; defaults to `{}` - -### Built-in Templates - -QuickThumb ships a small set of starter templates in `quickthumb/templates/`: - -| Name | Aspect Ratio | Description | -| ------------------ | ----------------- | --------------------------------------- | -| `youtube-16x9` | 16:9 (1280×720) | Title left, subject image right | -| `instagram-square` | 1:1 (1080×1080) | Centered headline with background image | -| `twitter-card` | 2:1 (1200×600) | Logo + title + subtitle | -| `og-image` | 1.91:1 (1200×630) | Open Graph social card | - -Access by name: - -```python -canvas = Canvas.from_template( - "youtube-16x9", - variables={"title": "My Video", "image": "thumb.jpg"}, -) -``` - -### Template Registry - -```python -from quickthumb import Canvas - -# Register a custom template by name -Canvas.register_template("my-brand", "/path/to/brand-template.json") - -# Use it by name -canvas = Canvas.from_template("my-brand", variables={"headline": "..."}) - -# Remove a registered template -Canvas.unregister_template("my-brand") -``` - -### Limitations - -- Substitution is string-level. Variables cannot inject JSON structure (objects, arrays). -- Variable values are always treated as strings. To inject a number into JSON, use `"size": $font_size` without surrounding quotes in the template, but the substituted value must be a valid JSON token. -- Unresolved placeholders (no matching variable) raise `ValidationError` before JSON parsing. -- `Canvas.to_json()` does not produce a template; templates are authored separately. - -### Notes - -- `Canvas.from_template()` raises `ValidationError` if required variables are missing. -- Built-in template names take lower precedence than user-registered names with the same key. - ---- - -## 3. Gradient / Image-Filled Text (Knockout Text) — `done` - -Fill text with a gradient or image instead of a flat color. The text shape acts as a mask that reveals the fill behind it. - -### Python API - -```python -from quickthumb import Canvas, LinearGradient, RadialGradient, TextFillImage, TextPart - -# Gradient-filled headline -canvas = Canvas(1280, 720).text( - content="GRADIENT TITLE", - size=120, - fill=LinearGradient( - angle=90, - stops=[("#FF6B6B", 0.0), ("#FFE66D", 0.5), ("#4ECDC4", 1.0)], - ), - position=("50%", "50%"), - align="center", -) - -# Image-filled text -canvas = Canvas(1280, 720).text( - content="FIRE TEXT", - size=140, - fill=TextFillImage(path="fire_texture.jpg", fit="cover"), - position=("50%", "50%"), - align="center", -) - -# Per-segment fills using TextPart -canvas = Canvas(1280, 720).text( - content=[ - TextPart( - text="HOT ", - fill=LinearGradient(angle=45, stops=[("#FF4500", 0.0), ("#FFD700", 1.0)]), - weight=900, - ), - TextPart( - text="COLD", - fill=LinearGradient(angle=45, stops=[("#00BFFF", 0.0), ("#8A2BE2", 1.0)]), - weight=900, - ), - ], - size=110, - position=("50%", "50%"), - align="center", -) -``` - -### `TextFillImage` Model - -```python -from quickthumb import TextFillImage - -fill = TextFillImage( - path="texture.jpg", # local path or remote URL - fit="cover", # "cover", "contain", or "fill" -) -``` - -Parameters: - -- `path`: required; local file path or remote URL -- `fit`: how the image is scaled to the text bounding box; default `"cover"` - -### Parameters (Text Layer and TextPart) - -New `fill` parameter on both `canvas.text(...)` and `TextPart`: - -- `fill`: `LinearGradient`, `RadialGradient`, or `TextFillImage`; mutually exclusive with `color` when set - -Fallback rule: if `fill` is `None`, `color` is used as before. - -### Implementation Notes - -- Render a white-on-black alpha mask from the text glyphs. -- Render the fill (gradient or image) onto a same-size canvas. -- Composite fill through the mask to produce the filled text image. -- Composite the result onto the main canvas, applying any layer-level effects (Stroke, Shadow, Glow) as usual. -- Effects operate on the filled text shape, not on the fill content itself. - -### JSON Serialization - -`fill` uses a `type` discriminator: - -```json -{ - "type": "text", - "content": "GRADIENT", - "size": 120, - "fill": { - "type": "linear_gradient", - "angle": 90, - "stops": [ - ["#FF6B6B", 0.0], - ["#4ECDC4", 1.0] - ] - }, - "position": ["50%", "50%"], - "align": "center", - "effects": [] -} -``` - -```json -{ - "type": "text", - "content": "TEXTURE", - "size": 140, - "fill": { - "type": "image", - "path": "fire_texture.jpg", - "fit": "cover" - }, - "position": ["50%", "50%"], - "align": "center", - "effects": [] -} -``` - -`TextFillImage` discriminator value: `"image"`. -`LinearGradient` discriminator value: `"linear_gradient"`. -`RadialGradient` discriminator value: `"radial_gradient"`. - -### Rules - -- `fill` and `color` are independent; `fill` takes visual precedence when set. -- `fill` on a `TextPart` overrides the layer-level `fill` for that segment only. -- `TextFillImage.path` supports remote URLs; the image is downloaded and cached at render time. -- `fit` on `TextFillImage` maps the image to the bounding box of the entire text block, not per-glyph. - ---- - -## 4. Noise / Grain Effect — `done` - -Add film-grain noise to backgrounds and images. - -### Per-Layer Effect - -`Grain` can be added to `effects` on background and image layers. - -```python -from quickthumb import Canvas, Grain - -canvas = ( - Canvas(1280, 720) - .background( - color="#1A1A2E", - effects=[Grain(intensity=0.12, monochrome=True)], - ) - .image( - path="portrait.png", - position=("70%", "50%"), - width=400, - height=500, - align=("center", "middle"), - effects=[Grain(intensity=0.08, monochrome=False, opacity=0.6)], - ) -) -``` - -### `Grain` Model - -```python -from quickthumb import Grain - -effect = Grain( - intensity=0.12, # 0.0 to 1.0; controls noise amplitude - monochrome=True, # True = luminance noise; False = color noise (RGB channels independently) - blend_mode="overlay", # blend mode for compositing noise onto the layer - opacity=1.0, # 0.0 to 1.0 -) -``` - -Parameters: - -- `intensity`: float from `0.0` to `1.0`; `0.0` produces no grain, `1.0` is maximum noise -- `monochrome`: bool; `True` generates identical noise across R/G/B channels (gray grain), `False` generates independent per-channel noise (color grain); default `True` -- `blend_mode`: how the noise layer is composited; `"overlay"`, `"screen"`, `"multiply"`, or `"normal"`; default `"overlay"` -- `opacity`: float from `0.0` to `1.0`; scales the grain strength; default `1.0` - -### JSON Serialization - -Per-layer effect: - -```json -{ - "type": "background", - "color": "#1A1A2E", - "effects": [ - { - "type": "grain", - "intensity": 0.12, - "monochrome": true, - "blend_mode": "overlay", - "opacity": 1.0 - } - ] -} -``` - -### Implementation Notes - -- Grain is generated using Pillow only; no NumPy dependency is introduced. -- Generate noise using `random.randint` into a raw bytes buffer and construct a Pillow `Image` from it, or use `ImageFilter` or point operations available in Pillow. -- Each render call generates a fresh noise sample (non-deterministic by default). -- Grain is generated at native canvas resolution; no supersampling. - -### Rules - -- `intensity` must be between `0.0` and `1.0`. -- `opacity` must be between `0.0` and `1.0`. -- `Grain` is valid in `effects` on background layers and image layers; it is not a valid text or shape effect. - ---- - -## 5. Presentation and Video — `exploratory` - -**This section is exploratory. Nothing here is committed for implementation.** - -These capabilities require significant additional dependencies and design work. They are documented here to capture direction and trade-offs, not as a near-term roadmap. - -### Slide Decks - -A `Deck` would be an ordered sequence of `Canvas` objects, each representing one slide. - -```python -# Hypothetical API — not implemented -from quickthumb import Canvas -from quickthumb_slides import Deck - -deck = Deck([ - Canvas(1280, 720).background(color="#0F172A").text(content="Slide 1", size=80, color="#FFF", position=("50%", "50%"), align="center"), - Canvas(1280, 720).background(color="#1E3A5F").text(content="Slide 2", size=80, color="#FFF", position=("50%", "50%"), align="center"), -]) - -deck.export_html("presentation/") # reveal.js bundle -deck.export_pptx("presentation.pptx") # requires python-pptx -``` - -Export targets: - -- **HTML**: a self-contained reveal.js bundle; each canvas renders to a PNG embedded as a slide background -- **PPTX**: via `python-pptx`; each canvas renders to a PNG inserted as a slide image - -### Video - -A video sequence would be a list of `Canvas` objects with per-frame timing and optional transitions. - -```python -# Hypothetical API — not implemented -from quickthumb_video import VideoSequence, Transition - -seq = VideoSequence(fps=30) -seq.add_frame(canvas_a, duration=3.0) -seq.add_transition(Transition.CROSSFADE, duration=0.5) -seq.add_frame(canvas_b, duration=3.0) -seq.render("output.mp4") # requires ffmpeg or moviepy -``` - -### Packaging Recommendation - -Both capabilities should ship as **separate packages** that depend on `quickthumb` core: - -- `quickthumb-slides` — slide deck export (HTML/PPTX) -- `quickthumb-video` — video sequence export (MP4) - -Reasons: - -- Avoids pulling large optional dependencies (`python-pptx`, `moviepy`, `ffmpeg` bindings) into `quickthumb` core. -- Keeps the core install fast and lightweight. -- Allows independent versioning and maintenance. -- Users who only need thumbnails pay no cost for video capabilities. - -### Open Questions - -- Transitions between canvases require interpolating layer states or blending rendered frames; which approach is tractable? -- PPTX export via rendered PNGs loses editability; is native PPTX shape generation worth the complexity? -- Should `Deck` support JSON serialization (a list of canvas JSON specs)? -- Audio tracks for video: out of scope for `quickthumb-video` or supported via ffmpeg passthrough? diff --git a/tests/test_docs_seo.py b/tests/test_docs_seo.py index b934e9b..4259d89 100644 --- a/tests/test_docs_seo.py +++ b/tests/test_docs_seo.py @@ -29,6 +29,6 @@ def test_docs_build_includes_search_engine_metadata(tmp_path): assert "Sitemap: https://sjquant.github.io/quickthumb/sitemap.xml" in ( site_dir / "robots.txt" ).read_text(encoding="utf-8") - assert '' in index_html + assert '' in index_html assert '' in index_html - assert '' in enums_html + assert '' in enums_html