Skip to content

Commit c1eed53

Browse files
authored
Merge pull request #1 from issackelly:add-img-attrs
Add img attrs option to `as_html` to match template tag behavior
2 parents 6b770df + e289f5d commit c1eed53

File tree

6 files changed

+97
-17
lines changed

6 files changed

+97
-17
lines changed

docs/api.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,12 @@ Template tags for rendering processed images:
102102

103103
<!-- With responsive sizes -->
104104
{% easy_image obj.image_field width=1200 sizes="(max-width: 768px) 600px, (max-width: 480px) 400px" %}
105-
```
105+
106+
<!-- With additional HTML attributes using img_ prefix -->
107+
{% img obj.image_field width="md" alt="Product" img_class="rounded-lg" img_loading="lazy" img_data_id="123" %}
108+
```
109+
110+
The template tag supports adding custom HTML attributes to the generated `<img>` element by prefixing them with `img_`. For example:
111+
- `img_class="my-class"` becomes `class="my-class"`
112+
- `img_loading="lazy"` becomes `loading="lazy"`
113+
- `img_data_id="123"` becomes `data-id="123"`

docs/getting-started.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ thumb = Img(width="md")
3232

3333
# Generate HTML for an image
3434
html = thumb(profile.photo, alt="Profile photo").as_html()
35+
36+
# Add extra attributes to the image
37+
html = thumb(profile.photo, alt="Profile photo").as_html(img_attrs={"loading": "lazy"})
3538
```
3639

3740
### Batch Processing for Multiple Images

easy_images/core.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -731,8 +731,11 @@ def sizes(self) -> str:
731731
# Ensure a string is returned
732732
return str(self._get_item_detail("sizes_attr", ""))
733733

734-
def as_html(self) -> str:
735-
"""Generate the complete <img> tag HTML with srcset and sizes."""
734+
def as_html(self, img_attrs: dict[str, str] | None = None) -> str:
735+
"""Generate the complete <img> tag HTML with srcset and sizes.
736+
737+
:param img_attrs: Optional dict of additional attributes to add to the <img> tag.
738+
"""
736739
# Accessing properties triggers loading if needed
737740
base_img = self.base
738741
srcset_items = self.srcset
@@ -774,6 +777,13 @@ def as_html(self) -> str:
774777
if base_img.height is not None:
775778
attrs.append(f'height="{base_img.height}"')
776779

780+
# Add any extra attributes from img_attrs
781+
if img_attrs:
782+
for k, v in img_attrs.items():
783+
# Avoid overwriting existing attributes
784+
if k and v and not any(attr.startswith(f"{k}=") for attr in attrs):
785+
attrs.append(f'{escape(k)}="{escape(v)}"')
786+
777787
# Filter out potential empty strings if base image failed etc.
778788
attrs = [attr for attr in attrs if attr]
779789

easy_images/engine.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99

1010
from django.core.files import File
1111
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile
12-
from django.db.models.fields.files import FieldFile
1312

1413
from easy_images.core import ParsedOptions
1514

@@ -138,15 +137,14 @@ def _new_image(file: str | Path | File, access, **kwargs):
138137

139138
path = None
140139
if isinstance(file, File):
141-
if isinstance(file, FieldFile):
142-
try:
143-
path = file.path
144-
except Exception:
145-
pass
146-
elif isinstance(file, TemporaryUploadedFile):
147-
path = file.temporary_file_path()
148-
if not path:
149-
path = getattr(file, "path", None)
140+
try:
141+
if isinstance(file, TemporaryUploadedFile):
142+
path = file.temporary_file_path()
143+
else:
144+
path = file.path # type: ignore
145+
except Exception:
146+
pass
147+
150148
if not path:
151149
content = file.read()
152150
if file.seekable():

tests/test_core.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,3 +201,34 @@ def get_image_side_effect_sizes(pk):
201201
assert "/img/size_400.webp 400w" in html_output # High density based on max width
202202
assert 'width="200"' in html_output
203203
assert 'height="112"' in html_output
204+
205+
206+
@pytest.mark.django_db
207+
@patch("easy_images.models.get_storage_name", return_value="default")
208+
def test_as_html_with_img_attrs(mock_get_storage):
209+
"""Test that as_html correctly adds custom img attributes."""
210+
# Create mock storage and assign URL
211+
mock_storage = Mock()
212+
mock_storage.url.return_value = "/test.jpg"
213+
214+
generator = Img(width=100)
215+
source = FieldFile(instance=EasyImage(), field=FileField(), name="test.jpg")
216+
source.storage = mock_storage # Assign mock storage to instance
217+
218+
bound_img = generator(source, alt="Test image")
219+
220+
# Test with custom img_attrs
221+
html = bound_img.as_html(img_attrs={"loading": "lazy", "class": "rounded", "data-id": "123"})
222+
223+
assert 'src="/test.jpg"' in html
224+
assert 'alt="Test image"' in html
225+
assert 'loading="lazy"' in html
226+
assert 'class="rounded"' in html
227+
assert 'data-id="123"' in html
228+
229+
# Test that existing attributes are not overwritten
230+
html2 = bound_img.as_html(img_attrs={"alt": "Should not override", "src": "should-not-override.jpg"})
231+
assert 'alt="Test image"' in html2 # Original alt should remain
232+
assert 'src="/test.jpg"' in html2 # Original src should remain
233+
assert 'alt="Should not override"' not in html2
234+
assert 'src="should-not-override.jpg"' not in html2

tests/test_engine.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
import tempfile
22
from pathlib import Path
33

4-
from django.core.files.uploadedfile import (
5-
SimpleUploadedFile,
6-
)
4+
from django.core.files.uploadedfile import SimpleUploadedFile
75

8-
from easy_images.engine import efficient_load, scale_image
6+
from easy_images.engine import _new_image, efficient_load, scale_image
97
from easy_images.options import ParsedOptions
108
from pyvips import Image
119

@@ -68,3 +66,35 @@ def test_scale():
6866

6967
scaled_not_upscale = scale_image(small_src, (400, 500), contain=True)
7068
assert (scaled_not_upscale.width, scaled_not_upscale.height) == (100, 100)
69+
70+
71+
def test_new_image_file_handling():
72+
"""Test the file read for different File backends."""
73+
74+
# Create a simple test image to work with
75+
image = Image.black(100, 100)
76+
with tempfile.TemporaryDirectory() as tmpdir:
77+
image_path = Path(tmpdir) / "test.jpg"
78+
image.write_to_file(str(image_path))
79+
80+
# Test with a path string - should work
81+
result = _new_image(str(image_path), "sequential")
82+
assert result is not None
83+
assert result.width == 100
84+
assert result.height == 100
85+
86+
# Test with a Path object - should also work
87+
result2 = _new_image(image_path, "sequential")
88+
assert result2 is not None
89+
assert result2.width == 100
90+
assert result2.height == 100
91+
92+
# Test with SimpleUploadedFile (uses read/buffer path)
93+
with open(image_path, "rb") as f:
94+
content = f.read()
95+
simple_file = SimpleUploadedFile("test.jpg", content, content_type="image/jpeg")
96+
97+
result3 = _new_image(simple_file, "sequential")
98+
assert result3 is not None
99+
assert result3.width == 100
100+
assert result3.height == 100

0 commit comments

Comments
 (0)