From e2132d5f7c328388424ab3626e32137f5b7b0375 Mon Sep 17 00:00:00 2001 From: Lonami Exo Date: Sat, 16 Oct 2021 13:56:38 +0200 Subject: [PATCH] Change the way thumb size selection works --- readthedocs/misc/v2-migration-guide.rst | 6 ++ telethon/_client/downloads.py | 63 +++++----------- telethon/_client/telegramclient.py | 45 +++++------- telethon/_misc/enums.py | 96 +++++++++++++++++++++++++ telethon/enums.py | 1 + 5 files changed, 139 insertions(+), 72 deletions(-) diff --git a/readthedocs/misc/v2-migration-guide.rst b/readthedocs/misc/v2-migration-guide.rst index c328549c..e4e111a4 100644 --- a/readthedocs/misc/v2-migration-guide.rst +++ b/readthedocs/misc/v2-migration-guide.rst @@ -706,3 +706,9 @@ formatting_entities stays because otherwise it's the only feasible way to manual todo update send_message and send_file docs (well review all functions) album overhaul. use a list of Message instead. + +size selector for download_profile_photo and download_media is now different + +still thumb because otherwise documents are weird. + +keep support for explicit size instance? diff --git a/telethon/_client/downloads.py b/telethon/_client/downloads.py index b29206d4..6339b8c9 100644 --- a/telethon/_client/downloads.py +++ b/telethon/_client/downloads.py @@ -7,7 +7,7 @@ import inspect import asyncio from .._crypto import AES -from .._misc import utils, helpers, requestiter, tlobject, hints +from .._misc import utils, helpers, requestiter, tlobject, hints, enums from .. import errors, _tl try: @@ -180,7 +180,7 @@ async def download_profile_photo( entity: 'hints.EntityLike', file: 'hints.FileLike' = None, *, - download_big: bool = True) -> typing.Optional[str]: + thumb) -> typing.Optional[str]: # hex(crc32(x.encode('ascii'))) for x in # ('User', 'Chat', 'UserFull', 'ChatFull') ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) @@ -189,8 +189,6 @@ async def download_profile_photo( if not isinstance(entity, tlobject.TLObject) or entity.SUBCLASS_OF_ID in INPUTS: entity = await self.get_entity(entity) - thumb = -1 if download_big else 0 - possible_names = [] if entity.SUBCLASS_OF_ID not in ENTITIES: photo = entity @@ -212,11 +210,13 @@ async def download_profile_photo( photo = entity.photo if isinstance(photo, (_tl.UserProfilePhoto, _tl.ChatPhoto)): + thumb = enums.Size.ORIGINAL if thumb == () else enums.parse_photo_size(thumb) + dc_id = photo.dc_id loc = _tl.InputPeerPhotoFileLocation( peer=await self.get_input_entity(entity), photo_id=photo.photo_id, - big=download_big + big=thumb >= enums.Size.LARGE ) else: # It doesn't make any sense to check if `photo` can be used @@ -259,7 +259,7 @@ async def download_media( message: 'hints.MessageLike', file: 'hints.FileLike' = None, *, - thumb: 'typing.Union[int, _tl.TypePhotoSize]' = None, + size = (), progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: # Downloading large documents may be slow enough to require a new file reference # to be obtained mid-download. Store (input chat, message id) so that the message @@ -292,11 +292,11 @@ async def download_media( return await _download_document( self, media, file, date, thumb, progress_callback, msg_data ) - elif isinstance(media, _tl.MessageMediaContact) and thumb is None: + elif isinstance(media, _tl.MessageMediaContact): return _download_contact( self, media, file ) - elif isinstance(media, (_tl.WebDocument, _tl.WebDocumentNoProxy)) and thumb is None: + elif isinstance(media, (_tl.WebDocument, _tl.WebDocumentNoProxy)): return await _download_web_document( self, media, file, progress_callback ) @@ -491,44 +491,15 @@ def _iter_download( def _get_thumb(thumbs, thumb): - # Seems Telegram has changed the order and put `PhotoStrippedSize` - # last while this is the smallest (layer 116). Ensure we have the - # sizes sorted correctly with a custom function. - def sort_thumbs(thumb): - if isinstance(thumb, _tl.PhotoStrippedSize): - return 1, len(thumb.bytes) - if isinstance(thumb, _tl.PhotoCachedSize): - return 1, len(thumb.bytes) - if isinstance(thumb, _tl.PhotoSize): - return 1, thumb.size - if isinstance(thumb, _tl.PhotoSizeProgressive): - return 1, max(thumb.sizes) - if isinstance(thumb, _tl.VideoSize): - return 2, thumb.size - - # Empty size or invalid should go last - return 0, 0 - - thumbs = list(sorted(thumbs, key=sort_thumbs)) - - for i in reversed(range(len(thumbs))): - # :tl:`PhotoPathSize` is used for animated stickers preview, and the thumb is actually - # a SVG path of the outline. Users expect thumbnails to be JPEG files, so pretend this - # thumb size doesn't actually exist (#1655). - if isinstance(thumbs[i], _tl.PhotoPathSize): - thumbs.pop(i) - - if thumb is None: - return thumbs[-1] - elif isinstance(thumb, int): - return thumbs[thumb] - elif isinstance(thumb, str): - return next((t for t in thumbs if t.type == thumb), None) - elif isinstance(thumb, (_tl.PhotoSize, _tl.PhotoCachedSize, - _tl.PhotoStrippedSize, _tl.VideoSize)): + if isinstance(thumb, tlobject.TLObject): return thumb - else: - return None + + thumb = enums.parse_photo_size(thumb) + return min( + thumbs, + default=None, + key=lambda t: abs(thumb - enums.parse_photo_size(t.type)) + ) def _download_cached_photo_size(self: 'TelegramClient', size, file): # No need to download anything, simply write the bytes @@ -623,7 +594,7 @@ async def _download_document( if not isinstance(document, _tl.Document): return - if thumb is None: + if thumb == (): kind, possible_names = _get_kind_and_names(document.attributes) file = _get_proper_filename( file, kind, utils.get_extension(document), diff --git a/telethon/_client/telegramclient.py b/telethon/_client/telegramclient.py index f2dc4ede..034b0a30 100644 --- a/telethon/_client/telegramclient.py +++ b/telethon/_client/telegramclient.py @@ -1610,7 +1610,7 @@ class TelegramClient: entity: 'hints.EntityLike', file: 'hints.FileLike' = None, *, - download_big: bool = True) -> typing.Optional[str]: + thumb: typing.Union[str, enums.Size] = ()) -> typing.Optional[str]: """ Downloads the profile photo from the given user, chat or channel. @@ -1634,8 +1634,15 @@ class TelegramClient: If file is the type `bytes`, it will be downloaded in-memory as a bytestring (e.g. ``file=bytes``). - download_big (`bool`, optional): - Whether to use the big version of the available photos. + thumb (optional): + The thumbnail size to download. A different size may be chosen + if the specified size doesn't exist. The category of the size + you choose will be respected when possible (e.g. if you + specify a cropped size, a cropped variant of similar size will + be preferred over a boxed variant of similar size). Cropped + images are considered to be smaller than boxed images. + + By default, the largest size (original) is downloaded. Returns `None` if no photo was provided, or if it was Empty. On success @@ -1655,7 +1662,7 @@ class TelegramClient: message: 'hints.MessageLike', file: 'hints.FileLike' = None, *, - thumb: 'typing.Union[int, _tl.TypePhotoSize]' = None, + thumb: typing.Union[str, enums.Size] = (), progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: """ Downloads the given media from a message object. @@ -1680,29 +1687,15 @@ class TelegramClient: A callback function accepting two parameters: ``(received bytes, total)``. - thumb (`int` | :tl:`PhotoSize`, optional): - Which thumbnail size from the document or photo to download, - instead of downloading the document or photo itself. + thumb (optional): + The thumbnail size to download. A different size may be chosen + if the specified size doesn't exist. The category of the size + you choose will be respected when possible (e.g. if you + specify a cropped size, a cropped variant of similar size will + be preferred over a boxed variant of similar size). Cropped + images are considered to be smaller than boxed images. - If it's specified but the file does not have a thumbnail, - this method will return `None`. - - The parameter should be an integer index between ``0`` and - ``len(sizes)``. ``0`` will download the smallest thumbnail, - and ``len(sizes) - 1`` will download the largest thumbnail. - You can also use negative indices, which work the same as - they do in Python's `list`. - - You can also pass the :tl:`PhotoSize` instance to use. - Alternatively, the thumb size type `str` may be used. - - In short, use ``thumb=0`` if you want the smallest thumbnail - and ``thumb=-1`` if you want the largest thumbnail. - - .. note:: - The largest thumbnail may be a video instead of a photo, - as they are available since layer 116 and are bigger than - any of the photos. + By default, the original media is downloaded. Returns `None` if no media was provided, or if it was Empty. On success diff --git a/telethon/_misc/enums.py b/telethon/_misc/enums.py index c8fa656b..2d5742aa 100644 --- a/telethon/_misc/enums.py +++ b/telethon/_misc/enums.py @@ -1,6 +1,16 @@ from enum import Enum +def _impl_op(which): + def op(self, other): + if not isinstance(other, type(self)): + return NotImplemented + + return getattr(self._val(), which)(other._val()) + + return op + + class ConnectionMode(Enum): FULL = 'full' INTERMEDIATE = 'intermediate' @@ -38,6 +48,91 @@ class Action(Enum): CANCEL = 'cancel' +class Size(Enum): + """ + See https://core.telegram.org/api/files#image-thumbnail-types. + + * ``'s'``. The image fits within a box of 100x100. + * ``'m'``. The image fits within a box of 320x320. + * ``'x'``. The image fits within a box of 800x800. + * ``'y'``. The image fits within a box of 1280x1280. + * ``'w'``. The image fits within a box of 2560x2560. + * ``'a'``. The image was cropped to be at most 160x160. + * ``'b'``. The image was cropped to be at most 320x320. + * ``'c'``. The image was cropped to be at most 640x640. + * ``'d'``. The image was cropped to be at most 1280x1280. + * ``'i'``. The image comes inline (no need to download anything). + * ``'j'``. Only the image outline is present (for stickers). + * ``'u'``. The image is actually a short MPEG4 animated video. + * ``'v'``. The image is actually a short MPEG4 video preview. + + The sorting order is first dimensions, then ``cropped < boxed < video < other``. + """ + SMALL = 's' + MEDIUM = 'm' + LARGE = 'x' + EXTRA_LARGE = 'y' + ORIGINAL = 'w' + CROPPED_SMALL = 'a' + CROPPED_MEDIUM = 'b' + CROPPED_LARGE = 'c' + CROPPED_EXTRA_LARGE = 'd' + INLINE = 'i' + OUTLINE = 'j' + ANIMATED = 'u' + VIDEO = 'v' + + def __hash__(self): + return object.__hash__(self) + + __sub__ = _impl_op('__sub__') + __lt__ = _impl_op('__lt__') + __le__ = _impl_op('__le__') + __eq__ = _impl_op('__eq__') + __ne__ = _impl_op('__ne__') + __gt__ = _impl_op('__gt__') + __ge__ = _impl_op('__ge__') + + def _val(self): + return self._category() * 100 + self._size() + + def _category(self): + return { + Size.SMALL: 2, + Size.MEDIUM: 2, + Size.LARGE: 2, + Size.EXTRA_LARGE: 2, + Size.ORIGINAL: 2, + Size.CROPPED_SMALL: 1, + Size.CROPPED_MEDIUM: 1, + Size.CROPPED_LARGE: 1, + Size.CROPPED_EXTRA_LARGE: 1, + Size.INLINE: 4, + Size.OUTLINE: 5, + Size.ANIMATED: 3, + Size.VIDEO: 3, + }[self] + + def _size(self): + return { + Size.SMALL: 1, + Size.MEDIUM: 3, + Size.LARGE: 5, + Size.EXTRA_LARGE: 6, + Size.ORIGINAL: 7, + Size.CROPPED_SMALL: 2, + Size.CROPPED_MEDIUM: 3, + Size.CROPPED_LARGE: 4, + Size.CROPPED_EXTRA_LARGE: 6, + # 0, since they're not the original photo at all + Size.INLINE: 0, + Size.OUTLINE: 0, + # same size as original or extra large (videos are large) + Size.ANIMATED: 7, + Size.VIDEO: 6, + }[self] + + def _mk_parser(cls): def parser(value): if isinstance(value, cls): @@ -57,3 +152,4 @@ def _mk_parser(cls): parse_conn_mode = _mk_parser(ConnectionMode) parse_participant = _mk_parser(Participant) parse_typing_action = _mk_parser(Action) +parse_photo_size = _mk_parser(Size) diff --git a/telethon/enums.py b/telethon/enums.py index bad39ea0..ef7715cc 100644 --- a/telethon/enums.py +++ b/telethon/enums.py @@ -2,4 +2,5 @@ from ._misc.enums import ( ConnectionMode, Participant, Action, + Size, )