Skip to content

Commit

Permalink
AtpRemoteBlob: add width and height properties
Browse files Browse the repository at this point in the history
populated for images, to be used in image embed aspectRatio. for snarfed/bridgy-fed#1571
  • Loading branch information
snarfed committed Jan 7, 2025
1 parent b301524 commit f5df258
Show file tree
Hide file tree
Showing 8 changed files with 48 additions and 6 deletions.
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,6 @@ Optional, only used in [com.atproto.repo](https://arroba.readthedocs.io/en/stabl

_Breaking changes:_

* `datastore_storage`:
* `DatastoreStorage`: add new `ndb_context_kwargs` constructor kwarg.
* `repo`:
* `apply_commit`, `apply_writes`: raise an exception if the repo is inactive.
* `storage`:
Expand All @@ -114,8 +112,12 @@ _Breaking changes:_

_Non-breaking changes:_
* `datastore_storage`:
* `apply_commit`: handle deactivated repos.
* `create_repo`: propagate `Repo.status` into `AtpRepo`.
* `DatastoreStorage`:
* Add new `ndb_context_kwargs` constructor kwarg.
* `apply_commit`: handle deactivated repos.
* `create_repo`: propagate `Repo.status` into `AtpRepo`.
* `AtpRemoteBlob`:
* Add `width` and `height` properties, populated for images, to be used in image embed `aspectRatio` ([snarfed/bridgy-fed#1571](https://github.com/snarfed/bridgy-fed/issues/1571)).


### 0.7 - 2024-11-08
Expand Down
15 changes: 15 additions & 0 deletions arroba/datastore_storage.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Google Cloud Datastore implementation of repo storage."""
from datetime import timezone
from functools import wraps
from io import BytesIO
import json
import logging
import mimetypes
Expand All @@ -15,6 +16,7 @@
from google.cloud.ndb.exceptions import ContextError
from lexrpc import ValidationError
from multiformats import CID, multicodec, multihash
from PIL import Image, UnidentifiedImageError

from .mst import MST
from .repo import Repo
Expand Down Expand Up @@ -298,6 +300,11 @@ class AtpRemoteBlob(ndb.Model):
size = ndb.IntegerProperty(required=True)
mime_type = ndb.StringProperty(required=True, default='application/octet-stream')

# only populated if mime_type is image/*
# used in images.aspectRatio in app.bsky.embed.images
width = ndb.IntegerProperty()
height = ndb.IntegerProperty()

created = ndb.DateTimeProperty(auto_now_add=True)
updated = ndb.DateTimeProperty(auto_now=True)

Expand Down Expand Up @@ -368,6 +375,14 @@ def validate_size(size):
blob = cls(id=url, cid=cid, size=len(resp.content))
if mime_type:
blob.mime_type = mime_type

if mime_type and mime_type.startswith('image/'):
try:
with Image.open(BytesIO(resp.content)) as image:
blob.width, blob.height = image.size
except UnidentifiedImageError as e:
logger.info(e)

blob.put()

# re-validate size in case the server didn't give us Content-Length.
Expand Down
1 change: 1 addition & 0 deletions arroba/tests/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
logging.basicConfig()
if '-v' in sys.argv:
logging.getLogger().setLevel(logging.DEBUG)
logging.getLogger('PIL').setLevel(logging.INFO)
elif 'discover' in sys.argv or '-q' in sys.argv or '--quiet' in sys.argv:
logging.disable(logging.CRITICAL + 1)

Binary file added arroba/tests/keyboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
17 changes: 17 additions & 0 deletions arroba/tests/test_datastore_storage.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Unit tests for datastore_storage.py."""
import os
from pathlib import Path
from unittest.mock import MagicMock, patch

from google.cloud import ndb
Expand Down Expand Up @@ -373,6 +374,22 @@ def test_create_remote_blob_infer_mime_type_from_url(self):
self.assertEqual(blob, got)
mock_get.assert_not_called()

def test_create_remote_blob_image_aspect_ratio(self):
image_bytes = Path(__file__).with_name('keyboard.png').read_bytes()
cid = CID.decode('bafkreicjoxic5d37v2ae3wfxkjgxtvx5hsp4ejjemaog322z5dvm7kgtvq')
mock_get = MagicMock(return_value=requests_response(image_bytes))

blob = AtpRemoteBlob.get_or_create(url='http://my/blob.png', get_fn=mock_get)
mock_get.assert_called_with('http://my/blob.png', stream=True)
self.assertEqual({
'$type': 'blob',
'ref': cid,
'mimeType': 'image/png',
'size': 9003,
}, blob.as_object())
self.assertEqual(21, blob.width)
self.assertEqual(12, blob.height)

def test_create_remote_blob_default_mime_type(self):
mock_get = MagicMock(return_value=requests_response('blob contents'))

Expand Down
9 changes: 7 additions & 2 deletions arroba/tests/testutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,13 +54,18 @@ def requests_response(body, status=200, headers=None):
if isinstance(body, (dict, list)):
resp.headers['content-type'] = 'application/json'
resp._text = json.dumps(body, indent=2)
else:
resp._content = resp._text.encode()
elif isinstance(body, str):
resp._text = body
resp._content = resp._text.encode()
elif isinstance(body, bytes):
resp._content = body
else:
assert False, f'unknown type for body: {type(body)}'

if headers:
resp.headers.update(headers)

resp._content = resp._text.encode()
resp.encoding = 'utf-8'
resp.status_code = status
return resp
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ dependencies = [
'dnspython>=2.0.0',
'lexrpc>=0.8',
'multiformats>=0.3.1',
'pillow',
'pyjwt>=2.0.0',
'simple-websocket',
]
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ MarkupSafe==2.1.3
multiformats==0.3.1.post4
multiformats-config==0.3.1
packaging==23.1
Pillow==11.0.0
proto-plus==1.23.0
protobuf==4.24.3
pyasn1==0.5.1
Expand Down

0 comments on commit f5df258

Please sign in to comment.