Skip to content

Commit f859cc7

Browse files
committed
docs: add README for script usage and setup
test: improve test mocks for DataFetcher fix: handle invalid gradient color in create_background ```
1 parent d380654 commit f859cc7

File tree

3 files changed

+120
-50
lines changed

3 files changed

+120
-50
lines changed

scripts/README.md

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# DevBcn scripts
2+
3+
This directory contains Python scripts for generating conference story images
4+
for speakers and sessions.
5+
6+
## Setup instructions
7+
8+
### Setting up a virtual environment
9+
10+
It's recommended to use a virtual environment to isolate the dependencies for
11+
these scripts.
12+
13+
```shell
14+
python3 -m venv venv
15+
16+
source venv/bin/activate
17+
```
18+
19+
### Installing dependencies
20+
21+
Once your virtual environment is activated, install the required dependencies
22+
using the requirements.txt file:
23+
24+
```bash
25+
pip install -r requirements.txt
26+
```
27+
28+
## Usage
29+
30+
The main script is `generate-story-images.py`, which generates story images for
31+
conference speakers and sessions.
32+
33+
```bash
34+
# Navigate to the scripts directory
35+
cd scripts
36+
37+
# Run the script with default settings
38+
python generate-story-images.py
39+
40+
# Run with custom settings
41+
python generate-story-images.py --output-dir custom_output --max-workers 5 --timeout 15
42+
```
43+
44+
### Available Options
45+
46+
- `--output-dir`: directory to save generated images (default: 'output')
47+
- `--max-workers`: maximum number of worker threads (default: 10)
48+
- `--timeout`: timeout for HTTP requests in seconds (default: 10)
49+
50+
## Project structure
51+
52+
- `config.py`: configuration settings for story image generation
53+
- `data_fetcher.py`: module for fetching data from APIs and local resources
54+
- `file_utils.py`: utilities for file operations
55+
- `image_processor.py`: image processing utilities
56+
- `story_generator.py`: story image generator module
57+
- `text_renderer.py`: module for text rendering operations
58+
- `tests/`: unit tests for the modules
59+
60+
## Deactivating the virtual environment
61+
62+
When you're done working with the scripts, you can deactivate the virtual
63+
environment:
64+
65+
```bash
66+
deactivate
67+
```

scripts/image_processor.py

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ class ImageProcessor:
1313

1414
def crop_to_square(self, image: Image.Image) -> Image.Image:
1515
"""Crop an image to a square shape, centered
16-
16+
1717
Args:
1818
image: The image to crop
19-
19+
2020
Returns:
2121
Cropped square image
2222
"""
@@ -26,93 +26,94 @@ def crop_to_square(self, image: Image.Image) -> Image.Image:
2626
top = (height - size) // 2
2727
right = left + size
2828
bottom = top + size
29-
29+
3030
return image.crop((left, top, right, bottom))
3131

3232
def resize_image(self, image: Image.Image, size: Tuple[int, int]) -> Image.Image:
3333
"""Resize an image
34-
34+
3535
Args:
3636
image: The image to resize
3737
size: Target size (width, height)
38-
38+
3939
Returns:
4040
Resized image
4141
"""
4242
return image.resize(size)
4343

4444
def apply_gradient_fade(self, image: Image.Image, fade_height: int) -> Image.Image:
4545
"""Apply a gradient fade to the bottom of an image
46-
46+
4747
Args:
4848
image: The image to process
4949
fade_height: Height of the fade effect in pixels
50-
50+
5151
Returns:
5252
Image with applied fade effect
5353
"""
5454
width, height = image.size
5555
fade = Image.new("L", (width, height), color=255)
56-
56+
5757
for y in range(height - fade_height, height):
5858
opacity = int(255 * (1 - (y - (height - fade_height)) / fade_height))
5959
for x in range(width):
6060
fade.putpixel((x, y), opacity)
61-
61+
6262
result = image.copy()
6363
result.putalpha(fade)
6464
return result
6565

6666
def create_background(self, size: Tuple[int, int], top_color: str, bottom_color: str) -> Image.Image:
6767
"""Create a gradient background
68-
68+
6969
Args:
7070
size: Image size (width, height)
7171
top_color: Hex color code for the top of the gradient
7272
bottom_color: Hex color code for the bottom of the gradient
73-
73+
7474
Returns:
7575
Image with gradient background
7676
"""
7777
try:
7878
bg = Image.new("RGBA", size, top_color)
7979
gradient = Image.new("RGBA", size)
8080
draw = ImageDraw.Draw(gradient)
81-
81+
8282
for y in range(size[1]):
8383
ratio = y / size[1]
8484
r = int(int(top_color[1:3], 16) * (1 - ratio) + int(bottom_color[1:3], 16) * ratio)
8585
g = int(int(top_color[3:5], 16) * (1 - ratio) + int(bottom_color[3:5], 16) * ratio)
8686
b = int(int(top_color[5:7], 16) * (1 - ratio) + int(bottom_color[5:7], 16) * ratio)
8787
draw.line([(0, y), (size[0], y)], fill=(r, g, b, 255))
88-
88+
8989
bg.alpha_composite(gradient)
9090
return bg
9191
except ValueError as e:
9292
logger.error(f"Error creating background: {e}")
9393
# Fallback to solid color if gradient fails
94-
return Image.new("RGBA", size, top_color)
94+
# Use a default color (#000000) instead of the potentially invalid top_color
95+
return Image.new("RGBA", size, "#000000")
9596

9697
def position_logo(self, base_image: Image.Image, logo: Image.Image,
9798
margin: int, logo_width_percentage: float = 0.2) -> Image.Image:
9899
"""Position a logo on the top right of the base image
99-
100+
100101
Args:
101102
base_image: The base image
102103
logo: The logo image
103104
margin: Margin from the edges
104105
logo_width_percentage: Width of the logo as a percentage of the base image width
105-
106+
106107
Returns:
107108
Image with logo placed on it
108109
"""
109110
base_width = base_image.width
110111
logo_width = int(base_width * logo_width_percentage)
111112
logo_height = int(logo.height * logo_width / logo.width)
112-
113+
113114
logo_resized = logo.resize((logo_width, logo_height))
114-
115+
115116
result = base_image.copy()
116117
result.paste(logo_resized, (base_width - logo_width - margin, margin), logo_resized)
117-
118+
118119
return result

scripts/tests/test_data_fetcher.py

Lines changed: 33 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -16,120 +16,122 @@
1616

1717
class TestDataFetcher(unittest.TestCase):
1818
"""Tests for the DataFetcher class"""
19-
19+
2020
def setUp(self):
2121
"""Set up test fixtures"""
2222
self.fetcher = DataFetcher(request_timeout=5)
23-
23+
2424
@patch('requests.get')
2525
def test_fetch_json_success(self, mock_get):
2626
"""Test successful JSON fetch"""
2727
# Mock response
2828
mock_response = Mock()
2929
mock_response.json.return_value = {"key": "value"}
3030
mock_get.return_value = mock_response
31-
31+
3232
# Test
3333
result = self.fetcher.fetch_json("http://example.com/api")
34-
34+
3535
# Verify
3636
mock_get.assert_called_once_with("http://example.com/api", timeout=5)
3737
mock_response.raise_for_status.assert_called_once()
3838
self.assertEqual(result, {"key": "value"})
39-
39+
4040
@patch('requests.get')
4141
def test_fetch_json_error(self, mock_get):
4242
"""Test JSON fetch with request error"""
4343
# Mock response
4444
mock_get.side_effect = requests.RequestException("Connection error")
45-
45+
4646
# Test
4747
with self.assertRaises(requests.RequestException):
4848
self.fetcher.fetch_json("http://example.com/api")
49-
49+
5050
@patch('requests.get')
5151
@patch('PIL.Image.open')
5252
def test_fetch_image_success(self, mock_open, mock_get):
5353
"""Test successful image fetch"""
5454
# Mock response
5555
mock_response = Mock()
56+
mock_response.content = b'fake_image_content'
5657
mock_get.return_value = mock_response
57-
58+
5859
# Mock image
5960
mock_image = MagicMock(spec=Image.Image)
6061
mock_image.convert.return_value = mock_image
6162
mock_open.return_value = mock_image
62-
63+
6364
# Test
6465
result = self.fetcher.fetch_image("http://example.com/image.jpg")
65-
66+
6667
# Verify
6768
mock_get.assert_called_once_with("http://example.com/image.jpg", timeout=5)
6869
mock_response.raise_for_status.assert_called_once()
6970
mock_image.convert.assert_called_once_with("RGBA")
7071
self.assertEqual(result, mock_image)
71-
72+
7273
@patch('requests.get')
7374
def test_fetch_image_request_error(self, mock_get):
7475
"""Test image fetch with request error"""
7576
# Mock response
7677
mock_get.side_effect = requests.RequestException("Connection error")
77-
78+
7879
# Test
7980
with self.assertRaises(requests.RequestException):
8081
self.fetcher.fetch_image("http://example.com/image.jpg")
81-
82+
8283
@patch('requests.get')
8384
@patch('PIL.Image.open')
8485
def test_fetch_image_processing_error(self, mock_open, mock_get):
8586
"""Test image fetch with image processing error"""
8687
# Mock response
8788
mock_response = Mock()
89+
mock_response.content = b'invalid_image_content'
8890
mock_get.return_value = mock_response
89-
91+
9092
# Mock image open error
9193
mock_open.side_effect = UnidentifiedImageError("invalid image")
92-
94+
9395
# Test
9496
with self.assertRaises(IOError):
9597
self.fetcher.fetch_image("http://example.com/image.jpg")
96-
98+
9799
@patch('PIL.Image.open')
98100
def test_load_local_image_success(self, mock_open):
99101
"""Test successful local image load"""
100102
# Mock image
101103
mock_image = MagicMock(spec=Image.Image)
102104
mock_image.convert.return_value = mock_image
103105
mock_open.return_value = mock_image
104-
106+
105107
# Test
106108
result = self.fetcher.load_local_image("path/to/image.png")
107-
109+
108110
# Verify
109111
mock_open.assert_called_once_with("path/to/image.png")
110112
mock_image.convert.assert_called_once_with("RGBA")
111113
self.assertEqual(result, mock_image)
112-
114+
113115
@patch('PIL.Image.open')
114116
def test_load_local_image_file_not_found(self, mock_open):
115117
"""Test local image load with file not found"""
116118
# Mock error
117119
mock_open.side_effect = FileNotFoundError("File not found")
118-
120+
119121
# Test
120122
with self.assertRaises(FileNotFoundError):
121123
self.fetcher.load_local_image("path/to/nonexistent.png")
122-
124+
123125
@patch('PIL.Image.open')
124126
def test_load_local_image_processing_error(self, mock_open):
125127
"""Test local image load with processing error"""
126128
# Mock error
127129
mock_open.side_effect = IOError("Invalid image")
128-
130+
129131
# Test
130132
with self.assertRaises(IOError):
131133
self.fetcher.load_local_image("path/to/invalid.png")
132-
134+
133135
@patch.object(DataFetcher, 'fetch_json')
134136
def test_fetch_speakers(self, mock_fetch_json):
135137
"""Test fetching speakers"""
@@ -138,17 +140,17 @@ def test_fetch_speakers(self, mock_fetch_json):
138140
{"id": "1", "name": "Speaker 1"},
139141
{"id": "2", "name": "Speaker 2"}
140142
]
141-
143+
142144
# Test
143145
result = self.fetcher.fetch_speakers("http://example.com/speakers")
144-
146+
145147
# Verify
146148
mock_fetch_json.assert_called_once_with("http://example.com/speakers")
147149
self.assertEqual(result, {
148150
"1": {"id": "1", "name": "Speaker 1"},
149151
"2": {"id": "2", "name": "Speaker 2"}
150152
})
151-
153+
152154
@patch.object(DataFetcher, 'fetch_json')
153155
def test_fetch_sessions_by_track(self, mock_fetch_json):
154156
"""Test fetching sessions by track"""
@@ -168,10 +170,10 @@ def test_fetch_sessions_by_track(self, mock_fetch_json):
168170
]
169171
}
170172
]
171-
173+
172174
# Test
173175
result = self.fetcher.fetch_sessions_by_track("http://example.com/sessions")
174-
176+
175177
# Verify
176178
mock_fetch_json.assert_called_once_with("http://example.com/sessions")
177179
self.assertEqual(result, {
@@ -183,16 +185,16 @@ def test_fetch_sessions_by_track(self, mock_fetch_json):
183185
{"id": "3", "title": "Session 3"}
184186
]
185187
})
186-
188+
187189
@patch.object(DataFetcher, 'fetch_json')
188190
def test_fetch_sessions_by_track_empty(self, mock_fetch_json):
189191
"""Test fetching sessions by track with empty data"""
190192
# Mock data
191193
mock_fetch_json.return_value = []
192-
194+
193195
# Test
194196
result = self.fetcher.fetch_sessions_by_track("http://example.com/sessions")
195-
197+
196198
# Verify
197199
mock_fetch_json.assert_called_once_with("http://example.com/sessions")
198200
self.assertEqual(result, {})

0 commit comments

Comments
 (0)