Change the way thumb size selection works

This commit is contained in:
Lonami Exo 2021-10-16 13:56:38 +02:00
parent 03de901b7f
commit e2132d5f7c
5 changed files with 139 additions and 72 deletions

View File

@ -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) todo update send_message and send_file docs (well review all functions)
album overhaul. use a list of Message instead. 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?

View File

@ -7,7 +7,7 @@ import inspect
import asyncio import asyncio
from .._crypto import AES 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 from .. import errors, _tl
try: try:
@ -180,7 +180,7 @@ async def download_profile_photo(
entity: 'hints.EntityLike', entity: 'hints.EntityLike',
file: 'hints.FileLike' = None, file: 'hints.FileLike' = None,
*, *,
download_big: bool = True) -> typing.Optional[str]: thumb) -> typing.Optional[str]:
# hex(crc32(x.encode('ascii'))) for x in # hex(crc32(x.encode('ascii'))) for x in
# ('User', 'Chat', 'UserFull', 'ChatFull') # ('User', 'Chat', 'UserFull', 'ChatFull')
ENTITIES = (0x2da17977, 0xc5af5d94, 0x1f4661b9, 0xd49a2697) 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: if not isinstance(entity, tlobject.TLObject) or entity.SUBCLASS_OF_ID in INPUTS:
entity = await self.get_entity(entity) entity = await self.get_entity(entity)
thumb = -1 if download_big else 0
possible_names = [] possible_names = []
if entity.SUBCLASS_OF_ID not in ENTITIES: if entity.SUBCLASS_OF_ID not in ENTITIES:
photo = entity photo = entity
@ -212,11 +210,13 @@ async def download_profile_photo(
photo = entity.photo photo = entity.photo
if isinstance(photo, (_tl.UserProfilePhoto, _tl.ChatPhoto)): if isinstance(photo, (_tl.UserProfilePhoto, _tl.ChatPhoto)):
thumb = enums.Size.ORIGINAL if thumb == () else enums.parse_photo_size(thumb)
dc_id = photo.dc_id dc_id = photo.dc_id
loc = _tl.InputPeerPhotoFileLocation( loc = _tl.InputPeerPhotoFileLocation(
peer=await self.get_input_entity(entity), peer=await self.get_input_entity(entity),
photo_id=photo.photo_id, photo_id=photo.photo_id,
big=download_big big=thumb >= enums.Size.LARGE
) )
else: else:
# It doesn't make any sense to check if `photo` can be used # It doesn't make any sense to check if `photo` can be used
@ -259,7 +259,7 @@ async def download_media(
message: 'hints.MessageLike', message: 'hints.MessageLike',
file: 'hints.FileLike' = None, file: 'hints.FileLike' = None,
*, *,
thumb: 'typing.Union[int, _tl.TypePhotoSize]' = None, size = (),
progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]: progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]:
# Downloading large documents may be slow enough to require a new file reference # 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 # 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( return await _download_document(
self, media, file, date, thumb, progress_callback, msg_data 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( return _download_contact(
self, media, file 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( return await _download_web_document(
self, media, file, progress_callback self, media, file, progress_callback
) )
@ -491,44 +491,15 @@ def _iter_download(
def _get_thumb(thumbs, thumb): def _get_thumb(thumbs, thumb):
# Seems Telegram has changed the order and put `PhotoStrippedSize` if isinstance(thumb, tlobject.TLObject):
# 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)):
return thumb 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): def _download_cached_photo_size(self: 'TelegramClient', size, file):
# No need to download anything, simply write the bytes # No need to download anything, simply write the bytes
@ -623,7 +594,7 @@ async def _download_document(
if not isinstance(document, _tl.Document): if not isinstance(document, _tl.Document):
return return
if thumb is None: if thumb == ():
kind, possible_names = _get_kind_and_names(document.attributes) kind, possible_names = _get_kind_and_names(document.attributes)
file = _get_proper_filename( file = _get_proper_filename(
file, kind, utils.get_extension(document), file, kind, utils.get_extension(document),

View File

@ -1610,7 +1610,7 @@ class TelegramClient:
entity: 'hints.EntityLike', entity: 'hints.EntityLike',
file: 'hints.FileLike' = None, 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. 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 If file is the type `bytes`, it will be downloaded in-memory
as a bytestring (e.g. ``file=bytes``). as a bytestring (e.g. ``file=bytes``).
download_big (`bool`, optional): thumb (optional):
Whether to use the big version of the available photos. 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 Returns
`None` if no photo was provided, or if it was Empty. On success `None` if no photo was provided, or if it was Empty. On success
@ -1655,7 +1662,7 @@ class TelegramClient:
message: 'hints.MessageLike', message: 'hints.MessageLike',
file: 'hints.FileLike' = None, 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]]: progress_callback: 'hints.ProgressCallback' = None) -> typing.Optional[typing.Union[str, bytes]]:
""" """
Downloads the given media from a message object. Downloads the given media from a message object.
@ -1680,29 +1687,15 @@ class TelegramClient:
A callback function accepting two parameters: A callback function accepting two parameters:
``(received bytes, total)``. ``(received bytes, total)``.
thumb (`int` | :tl:`PhotoSize`, optional): thumb (optional):
Which thumbnail size from the document or photo to download, The thumbnail size to download. A different size may be chosen
instead of downloading the document or photo itself. 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, By default, the original media is downloaded.
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.
Returns Returns
`None` if no media was provided, or if it was Empty. On success `None` if no media was provided, or if it was Empty. On success

View File

@ -1,6 +1,16 @@
from enum import Enum 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): class ConnectionMode(Enum):
FULL = 'full' FULL = 'full'
INTERMEDIATE = 'intermediate' INTERMEDIATE = 'intermediate'
@ -38,6 +48,91 @@ class Action(Enum):
CANCEL = 'cancel' 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 _mk_parser(cls):
def parser(value): def parser(value):
if isinstance(value, cls): if isinstance(value, cls):
@ -57,3 +152,4 @@ def _mk_parser(cls):
parse_conn_mode = _mk_parser(ConnectionMode) parse_conn_mode = _mk_parser(ConnectionMode)
parse_participant = _mk_parser(Participant) parse_participant = _mk_parser(Participant)
parse_typing_action = _mk_parser(Action) parse_typing_action = _mk_parser(Action)
parse_photo_size = _mk_parser(Size)

View File

@ -2,4 +2,5 @@ from ._misc.enums import (
ConnectionMode, ConnectionMode,
Participant, Participant,
Action, Action,
Size,
) )