quickthumb 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. quickthumb-0.1.0/.claude/agents/tdd-implementer.md +41 -0
  2. quickthumb-0.1.0/.claude/agents/tdd-test-writer.md +50 -0
  3. quickthumb-0.1.0/.claude/settings.json +34 -0
  4. quickthumb-0.1.0/.github/workflows/publish.yaml +74 -0
  5. quickthumb-0.1.0/.gitignore +162 -0
  6. quickthumb-0.1.0/.vscode/extensions.json +3 -0
  7. quickthumb-0.1.0/.vscode/settings.json +13 -0
  8. quickthumb-0.1.0/CLAUDE.md +107 -0
  9. quickthumb-0.1.0/DESIGN.md +400 -0
  10. quickthumb-0.1.0/LICENSE +21 -0
  11. quickthumb-0.1.0/PKG-INFO +293 -0
  12. quickthumb-0.1.0/PROGRESS.md +66 -0
  13. quickthumb-0.1.0/README.md +280 -0
  14. quickthumb-0.1.0/examples/README.md +18 -0
  15. quickthumb-0.1.0/examples/assets/c-g-JgDUVGAXsso-unsplash.jpg +0 -0
  16. quickthumb-0.1.0/examples/youtube_thumbnail_01.png +0 -0
  17. quickthumb-0.1.0/examples/youtube_thumbnail_01.py +49 -0
  18. quickthumb-0.1.0/pyproject.toml +81 -0
  19. quickthumb-0.1.0/quickthumb/__init__.py +32 -0
  20. quickthumb-0.1.0/quickthumb/canvas.py +1183 -0
  21. quickthumb-0.1.0/quickthumb/errors.py +10 -0
  22. quickthumb-0.1.0/quickthumb/models.py +374 -0
  23. quickthumb-0.1.0/tests/__init__.py +1 -0
  24. quickthumb-0.1.0/tests/conftest.py +41 -0
  25. quickthumb-0.1.0/tests/fixtures/sample_image.jpg +0 -0
  26. quickthumb-0.1.0/tests/snapshots/arial_bold.png +0 -0
  27. quickthumb-0.1.0/tests/snapshots/arial_bold_italic.png +0 -0
  28. quickthumb-0.1.0/tests/snapshots/arial_italic.png +0 -0
  29. quickthumb-0.1.0/tests/snapshots/blend_mode_darken.png +0 -0
  30. quickthumb-0.1.0/tests/snapshots/blend_mode_lighten.png +0 -0
  31. quickthumb-0.1.0/tests/snapshots/blend_mode_multiply.png +0 -0
  32. quickthumb-0.1.0/tests/snapshots/blend_mode_normal.png +0 -0
  33. quickthumb-0.1.0/tests/snapshots/blend_mode_overlay.png +0 -0
  34. quickthumb-0.1.0/tests/snapshots/blend_mode_screen.png +0 -0
  35. quickthumb-0.1.0/tests/snapshots/bold_italic_with_named_font.png +0 -0
  36. quickthumb-0.1.0/tests/snapshots/bold_with_named_font.png +0 -0
  37. quickthumb-0.1.0/tests/snapshots/font_name_works.png +0 -0
  38. quickthumb-0.1.0/tests/snapshots/image_background_basic.png +0 -0
  39. quickthumb-0.1.0/tests/snapshots/image_background_with_opacity.png +0 -0
  40. quickthumb-0.1.0/tests/snapshots/image_brightness_decrease.png +0 -0
  41. quickthumb-0.1.0/tests/snapshots/image_brightness_increase.png +0 -0
  42. quickthumb-0.1.0/tests/snapshots/image_fit_contain.png +0 -0
  43. quickthumb-0.1.0/tests/snapshots/image_fit_cover.png +0 -0
  44. quickthumb-0.1.0/tests/snapshots/image_fit_fill.png +0 -0
  45. quickthumb-0.1.0/tests/snapshots/italic_with_named_font.png +0 -0
  46. quickthumb-0.1.0/tests/snapshots/jpeg_format.jpg +0 -0
  47. quickthumb-0.1.0/tests/snapshots/linear_gradient_brightness_decrease.png +0 -0
  48. quickthumb-0.1.0/tests/snapshots/linear_gradient_brightness_increase.png +0 -0
  49. quickthumb-0.1.0/tests/snapshots/linear_gradient_diagonal.png +0 -0
  50. quickthumb-0.1.0/tests/snapshots/linear_gradient_horizontal.png +0 -0
  51. quickthumb-0.1.0/tests/snapshots/linear_gradient_with_opacity.png +0 -0
  52. quickthumb-0.1.0/tests/snapshots/outline_basic.png +0 -0
  53. quickthumb-0.1.0/tests/snapshots/outline_with_offset.png +0 -0
  54. quickthumb-0.1.0/tests/snapshots/percentage_with_alignment.png +0 -0
  55. quickthumb-0.1.0/tests/snapshots/radial_gradient_brightness_decrease.png +0 -0
  56. quickthumb-0.1.0/tests/snapshots/radial_gradient_brightness_increase.png +0 -0
  57. quickthumb-0.1.0/tests/snapshots/radial_gradient_centered.png +0 -0
  58. quickthumb-0.1.0/tests/snapshots/radial_gradient_with_opacity.png +0 -0
  59. quickthumb-0.1.0/tests/snapshots/rich_text_advanced_styles.png +0 -0
  60. quickthumb-0.1.0/tests/snapshots/rich_text_alignment.png +0 -0
  61. quickthumb-0.1.0/tests/snapshots/rich_text_different_colors.png +0 -0
  62. quickthumb-0.1.0/tests/snapshots/rich_text_mixed_fonts.png +0 -0
  63. quickthumb-0.1.0/tests/snapshots/rich_text_mixed_styles.png +0 -0
  64. quickthumb-0.1.0/tests/snapshots/rich_text_with_effects.png +0 -0
  65. quickthumb-0.1.0/tests/snapshots/solid_background.png +0 -0
  66. quickthumb-0.1.0/tests/snapshots/solid_color_brightness_decrease.png +0 -0
  67. quickthumb-0.1.0/tests/snapshots/solid_color_brightness_increase.png +0 -0
  68. quickthumb-0.1.0/tests/snapshots/text_alignment.png +0 -0
  69. quickthumb-0.1.0/tests/snapshots/text_bold_and_italic.png +0 -0
  70. quickthumb-0.1.0/tests/snapshots/text_no_wrapping.png +0 -0
  71. quickthumb-0.1.0/tests/snapshots/text_rendering.png +0 -0
  72. quickthumb-0.1.0/tests/snapshots/text_with_basic_shadow.png +0 -0
  73. quickthumb-0.1.0/tests/snapshots/text_with_blurred_shadow.png +0 -0
  74. quickthumb-0.1.0/tests/snapshots/text_with_glow.png +0 -0
  75. quickthumb-0.1.0/tests/snapshots/text_with_letter_spacing.png +0 -0
  76. quickthumb-0.1.0/tests/snapshots/text_with_line_height.png +0 -0
  77. quickthumb-0.1.0/tests/snapshots/text_with_multiple_glows.png +0 -0
  78. quickthumb-0.1.0/tests/snapshots/text_with_stroke.png +0 -0
  79. quickthumb-0.1.0/tests/snapshots/text_wrapping_center_aligned.png +0 -0
  80. quickthumb-0.1.0/tests/snapshots/text_wrapping_center_aligned_percentage.png +0 -0
  81. quickthumb-0.1.0/tests/snapshots/text_wrapping_left_aligned.png +0 -0
  82. quickthumb-0.1.0/tests/snapshots/text_wrapping_right_aligned.png +0 -0
  83. quickthumb-0.1.0/tests/snapshots/times_new_roman_bold.png +0 -0
  84. quickthumb-0.1.0/tests/snapshots/times_new_roman_bold_italic.png +0 -0
  85. quickthumb-0.1.0/tests/snapshots/times_new_roman_italic.png +0 -0
  86. quickthumb-0.1.0/tests/snapshots/webp_format.webp +0 -0
  87. quickthumb-0.1.0/tests/test_background_layers.py +384 -0
  88. quickthumb-0.1.0/tests/test_canvas.py +164 -0
  89. quickthumb-0.1.0/tests/test_outline_layers.py +153 -0
  90. quickthumb-0.1.0/tests/test_rendering.py +1162 -0
  91. quickthumb-0.1.0/tests/test_text_effects.py +410 -0
  92. quickthumb-0.1.0/tests/test_text_layers.py +508 -0
  93. quickthumb-0.1.0/uv.lock +626 -0
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: tdd-implementer
3
+ description: "Use this agent when you have existing failing tests and need to generate clean, self-explanatory production code that rigorously follows the TDD 'Green' and 'Refactor' phases."
4
+ model: sonnet
5
+ color: blue
6
+ ---
7
+
8
+ You are a Senior Software Engineer specializing in **TDD (Test-Driven Development)**. Your goal is to implement business logic to pass existing failing tests (Green Phase) and then optimize the code (Refactor Phase).
9
+
10
+ ## Context
11
+
12
+ You must write the production code to make the tests pass, adhering strictly to the operational constraints defined below.
13
+
14
+ ## Operational Constraints
15
+
16
+ 1. **Self-Explanatory Code:**
17
+ - **DO NOT** write comments explaining "what" or "how".
18
+ - Code must be readable solely through descriptive variable names, function names, and clear logic flow.
19
+ - _Exception:_ Docstrings for public API documentation are allowed if required by the language standard, but keep them minimal.
20
+ 2. **Zero-Noise Error Handling:**
21
+ - **DO NOT** add `try-catch` blocks solely for logging purposes.
22
+ 3. **TDD Workflow (Green -> Refactor):**
23
+ - **Step 1 (Green):** Write the minimum code necessary to pass the test.
24
+ - **Step 2 (Refactor):** Optimize stricture, eliminate duplication, and improve readability without changing behavior.
25
+ 4. **Black-Box Testing (Encapsulation)**:
26
+ - DO NOT access or assert against private properties or methods in tests.
27
+ - Tests must verify behavior strictly through the public API/Interface.
28
+ 5. **Implementation Volume Control:**
29
+ - **BEFORE** generating code, estimate the required lines of implementation code (excluding test code volume).
30
+ - If the implementation change requires **>300 lines** (and is NOT a simple repetitive task), **STOP** and **ASK** me to confirm.
31
+
32
+ Before presenting code, verify:
33
+
34
+ - [ ] All tests pass
35
+ - [ ] No explanatory comments exist (except minimal docstrings if needed)
36
+ - [ ] No try-catch blocks for logging only
37
+ - [ ] All names are self-explanatory
38
+ - [ ] Code follows TDD Green-Refactor cycle
39
+ - [ ] User was consulted if >300 lines (non-repetitive)
40
+ - [ ] No premature optimization
41
+ - [ ] Code is as simple as possible, but no simpler
@@ -0,0 +1,50 @@
1
+ ---
2
+ name: tdd-test-writer
3
+ description: "Use this agent when you need to translate business requirements into structured, failing test cases to rigorously initiate the 'Red' phase of Test-Driven Development."
4
+ model: sonnet
5
+ color: red
6
+ ---
7
+
8
+ ## Role
9
+
10
+ You are an expert Software QA Engineer and TDD (Test-Driven Development) Specialist. Your goal is to convert business requirements into high-quality, failing test cases (The "Red" phase of TDD).
11
+
12
+ ## Instructions
13
+
14
+ 1. **Analyze Requirements:** deeply understand the provided requirements to identify happy paths, edge cases, and potential error states.
15
+ 2. **Define Test Scope:** Create "Meaningful Tests" only. Avoid redundant or trivial tests.
16
+ 3. **Draft Test Cases:** Write the test code following the constraints below.
17
+
18
+ ## Filtering Criteria
19
+
20
+ ### 1. ❌ IGNORE / DO NOT TEST
21
+ * **Language Syntax:** Do not test if the programming language works (e.g., method chaining returning `self`, simple type instantiation).
22
+ * **Third-Party Libraries:** Do not test if standard libraries (JSON, HTTP clients, ORMs) work. Assume they work.
23
+ * **Trivial Data Access:** Do not test simple Getters/Setters unless they contain transformation logic.
24
+ * **Redundant Checks:** If a "Happy Path" test already verifies a result, do not create a separate test just to check a sub-property of that same result.
25
+ * **Parameter Variations:** Do not create multiple tests for different parameter values when one test suffices to verify the feature works.
26
+ * **Feature Combinations:** Do not test combinations of already-tested features unless there's specific integration logic.
27
+ * **Implementation Detail:** Do not test internal optimizations, private methods, or internal state
28
+
29
+ ### 2. ✅ MUST TEST
30
+ * **Business Invariants:** Rules that must always be true (e.g., "Balance cannot be negative", "Username must be unique").
31
+ * **State Transitions:** Verifying allowed and disallowed status changes (e.g., "Cannot refund a cancelled order").
32
+ * **Boundary Analysis:** Edge cases at the limits of logic (e.g., 0, Max Int, Empty Lists, Leap Years).
33
+ * **Error Handling:** Custom exceptions and fallback logic defined by the business requirements.
34
+ * **Distinct Feature Parameters:** If a parameter changes the behavior significantly, test one variation to prove the parameter works.
35
+
36
+ ## Constraints & Standards
37
+
38
+ 1. **TDD Red Phase Only:**
39
+ - All tests MUST fail initially.
40
+ - **DO NOT** implement the business logic or functional code.
41
+ - Inside the test function, simply call a failure method.
42
+ 2. **Structure (Given-When-Then):**
43
+ - You must explicitly comment the sections within each test function:
44
+ - `Given`: Setup initial state/mocks.
45
+ - `When`: Execute the action.
46
+ - `Then`: Assert the expected result.
47
+ 3. **Documentation:**
48
+ - Every test function must have a clear **Docstring** summarizing the test scenario and expected outcome.
49
+ 4. **Naming Convention:**
50
+ - Use descriptive names reflecting the behavior (e.g., `test_should_return_error_when_input_is_negative`).
@@ -0,0 +1,34 @@
1
+ {
2
+ "hooks": {
3
+ "PostToolUse": [
4
+ {
5
+ "matcher": "Write|Edit",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "jq -r '.tool_input.file_path | select(endswith(\".py\"))' | xargs -r uv tool run ruff format >&2 || { [ $? -eq 1 ] && exit 2; }"
10
+ },
11
+ {
12
+ "type": "command",
13
+ "command": "jq -r '.tool_input.file_path | select(endswith(\".py\"))' | xargs -r uv tool run ruff check --fix >&2 || { [ $? -eq 1 ] && exit 2; }"
14
+ },
15
+ {
16
+ "type": "command",
17
+ "command": "jq -r '.tool_input.file_path | select(endswith(\".py\"))' | xargs -r uv tool run ty check >&2 || { [ $? -eq 1 ] && exit 2; }"
18
+ }
19
+ ]
20
+ }
21
+ ],
22
+ "Notification": [
23
+ {
24
+ "matcher": "",
25
+ "hooks": [
26
+ {
27
+ "type": "command",
28
+ "command": "terminal-notifier -title 'Claude Code' -message 'Waiting for your input' -sound Glass"
29
+ }
30
+ ]
31
+ }
32
+ ]
33
+ }
34
+ }
@@ -0,0 +1,74 @@
1
+ name: Automated Release Process
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ contents: write
13
+ id-token: write
14
+ steps:
15
+ - name: Checkout repository
16
+ uses: actions/checkout@v4
17
+
18
+ - name: Install uv
19
+ uses: astral-sh/setup-uv@v5
20
+ with:
21
+ enable-cache: true
22
+ python-version: "3.10"
23
+
24
+ - name: Test
25
+ run: |
26
+ uv run pytest tests/ --ignore=tests/test_rendering.py
27
+
28
+ - name: Determine Version Change
29
+ id: version_check
30
+ run: |
31
+ VERSION="v$(uv version --short)"
32
+ echo "Current version: $VERSION"
33
+
34
+ LATEST_RELEASE=$(curl -s -H "Authorization: token ${{ secrets.GITHUB_TOKEN }}" \
35
+ https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r '.tag_name')
36
+ echo "Latest release version: $LATEST_RELEASE"
37
+
38
+ if [ "$VERSION" != "$LATEST_RELEASE" ]; then
39
+ echo "version_changed=true" >> $GITHUB_OUTPUT
40
+ echo "new_version=$VERSION" >> $GITHUB_OUTPUT
41
+ else
42
+ echo "version_changed=false" >> $GITHUB_OUTPUT
43
+ fi
44
+
45
+ - name: Create Release
46
+ if: steps.version_check.outputs.version_changed == 'true'
47
+ uses: softprops/action-gh-release@v2
48
+ with:
49
+ tag_name: ${{ steps.version_check.outputs.new_version }}
50
+ generate_release_notes: True
51
+
52
+ - name: mint API token
53
+ id: mint-token
54
+ run: |
55
+ # retrieve the ambient OIDC token
56
+ resp=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
57
+ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=pypi")
58
+ oidc_token=$(jq -r '.value' <<< "${resp}")
59
+
60
+ # exchange the OIDC token for an API token
61
+ resp=$(curl -X POST https://pypi.org/_/oidc/mint-token -d "{\"token\": \"${oidc_token}\"}")
62
+ api_token=$(jq -r '.token' <<< "${resp}")
63
+
64
+ # mask the newly minted API token, so that we don't accidentally leak it
65
+ echo "::add-mask::${api_token}"
66
+
67
+ # see the next step in the workflow for an example of using this step output
68
+ echo "api-token=${api_token}" >> "${GITHUB_OUTPUT}"
69
+
70
+ - name: Build and publish to PyPI
71
+ if: steps.version_check.outputs.version_changed == 'true'
72
+ run: |
73
+ uv build
74
+ uv publish --token ${{ steps.mint-token.outputs.api-token }}
@@ -0,0 +1,162 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # poetry
98
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
102
+ #poetry.lock
103
+
104
+ # pdm
105
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
106
+ #pdm.lock
107
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
108
+ # in version control.
109
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
110
+ .pdm.toml
111
+ .pdm-python
112
+ .pdm-build/
113
+
114
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
115
+ __pypackages__/
116
+
117
+ # Celery stuff
118
+ celerybeat-schedule
119
+ celerybeat.pid
120
+
121
+ # SageMath parsed files
122
+ *.sage.py
123
+
124
+ # Environments
125
+ .env
126
+ .venv
127
+ env/
128
+ venv/
129
+ ENV/
130
+ env.bak/
131
+ venv.bak/
132
+
133
+ # Spyder project settings
134
+ .spyderproject
135
+ .spyproject
136
+
137
+ # Rope project settings
138
+ .ropeproject
139
+
140
+ # mkdocs documentation
141
+ /site
142
+
143
+ # mypy
144
+ .mypy_cache/
145
+ .dmypy.json
146
+ dmypy.json
147
+
148
+ # Pyre type checker
149
+ .pyre/
150
+
151
+ # pytype static type analyzer
152
+ .pytype/
153
+
154
+ # Cython debug symbols
155
+ cython_debug/
156
+
157
+ # PyCharm
158
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
159
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
160
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
161
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
162
+ #.idea/
@@ -0,0 +1,3 @@
1
+ {
2
+ "recommendations": ["charliermarsh.ruff", "charliermarsh.ty"]
3
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "[python]": {
3
+ "editor.codeActionsOnSave": {
4
+ "source.fixAll": "explicit",
5
+ "source.organizeImports": "explicit"
6
+ },
7
+ "editor.defaultFormatter": "charliermarsh.ruff"
8
+ },
9
+ "ty.importStrategy": "fromEnvironment",
10
+ "ruff.importStrategy": "fromEnvironment",
11
+ "python.testing.unittestEnabled": false,
12
+ "python.testing.pytestEnabled": true
13
+ }
@@ -0,0 +1,107 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## Project Overview
6
+
7
+ QuickThumb is a Python library for programmatic thumbnail generation. See [README.md](README.md) for features and examples, [DESIGN.md](DESIGN.md) for API specifications and design principles.
8
+
9
+ ## Project Structure
10
+
11
+ ```
12
+ .
13
+ ├── CLAUDE.md
14
+ ├── DESIGN.md
15
+ ├── LICENSE
16
+ ├── README.md
17
+ ├── examples
18
+ │ ├── README.md
19
+ │ └── youtube_thumbnail.py
20
+ ├── pyproject.toml
21
+ ├── quickthumb
22
+ │ ├── __init__.py
23
+ │ ├── canvas.py
24
+ │ ├── errors.py
25
+ │ └── models.py
26
+ ├── tests
27
+ │ ├── __init__.py
28
+ │ ├── test_background_layers.py
29
+ │ ├── test_canvas.py
30
+ │ └── test_text_layers.py
31
+ └── uv.lock
32
+ ```
33
+
34
+ ## Development Commands
35
+
36
+ ### Setup
37
+
38
+ ```bash
39
+ # Install dependencies
40
+ uv sync
41
+ ```
42
+
43
+ ### Testing
44
+
45
+ ```bash
46
+ # Run all tests
47
+ uv run pytest tests/ -v
48
+
49
+ # Run single test file
50
+ uv run pytest tests/test_canvas_creation.py -v
51
+
52
+ # Run specific test
53
+ uv run pytest tests/test_canvas_creation.py::TestCanvasCreation::test_should_create_canvas_with_explicit_dimensions -v
54
+
55
+ # Run with coverage report
56
+ uv run pytest tests/ --cov=quickthumb --cov-report=html
57
+ ```
58
+
59
+ ### Code Quality
60
+
61
+ ```bash
62
+ # Type checking
63
+ uv run ty check quickthumb/
64
+
65
+ # Linting
66
+ uv run ruff check quickthumb/
67
+
68
+ # Auto-fix linting issues
69
+ uv run ruff check --fix quickthumb/
70
+
71
+ # Format code
72
+ uv run ruff format quickthumb/
73
+ ```
74
+
75
+ ## Architecture
76
+
77
+ ### Module Organization
78
+
79
+ - **`models.py`**: Pydantic models for validation and serialization
80
+ - `LinearGradient`: Gradient configuration with type, angle, stops
81
+ - `BackgroundLayer`: Background layer with color/gradient/image
82
+ - `TextLayer`: Text layer with content, styling, positioning
83
+ - `CanvasModel`: Canvas serialization model (width, height, layers)
84
+ - **`canvas.py`**: Canvas class with method chaining API
85
+ - **`errors.py`**: Custom exceptions (`ValidationError`, `QuickthumbError`)
86
+
87
+ ### Layer Stack Model
88
+
89
+ Layers are appended to `Canvas._layers` in call order and rendered sequentially:
90
+
91
+ 1. Background layers (stackable with blend modes)
92
+ 2. Text layers
93
+ 3. Decoration layers
94
+
95
+ ### Dual API
96
+
97
+ Both Python method chaining and JSON configuration map to the same internal `Canvas._layers` structure.
98
+
99
+ ## Testing Philosophy
100
+
101
+ Follows **strict TDD** as defined in `.claude/agents/tdd-implementer.md`:
102
+
103
+ - Self-explanatory code (no "what"/"how" comments)
104
+ - Black-box testing (public API only)
105
+ - Green → Refactor cycle
106
+
107
+ Test naming: `test_should_{expected_behavior}_{optional_context}`