@@ -39,6 +39,7 @@ import os
3939import pathlib
4040import sys
4141import traceback
42+ import warnings
4243from collections import OrderedDict
4344from types import ModuleType
4445from typing import Dict, Generator, Iterator, List, Optional, Set, TextIO, Tuple, Union
@@ -373,20 +374,34 @@ cdef class _Creator:
373374 self.c_creator.setMainPath(mainPath.encode('UTF-8'))
374375 return self
375376
376- def add_illustration(self, int size: pyint , content: bytes):
377+ def add_illustration(self, size_or_info , content: bytes):
377378 """ Add a PNG illustration to Archive.
378379
379380 Refer to https:// wiki.openzim.org/ wiki/ Metadata for more details.
380381
381382 Args:
382- size (int ): The width of the square PNG illustration in pixels.
383+ size_or_info: Either an int (width of the square PNG illustration in pixels)
384+ or an IllustrationInfo object with width, height, and scale.
383385 content (bytes): The binary content of the PNG illustration.
384386
385387 Raises:
386- RuntimeError : If an illustration with the same width already exists.
388+ RuntimeError : If an illustration with the same attributes already exists.
389+
390+ Examples:
391+ # Old style (square illustration at scale 1)
392+ creator.add_illustration(48 , png_data)
393+
394+ # New style (with dimensions and scale)
395+ info = IllustrationInfo(48 , 48 , 2.0 )
396+ creator.add_illustration(info, png_data)
387397 """
388398 cdef string _content = content
389- self.c_creator.addIllustration(size, _content)
399+ if isinstance(size_or_info, IllustrationInfo):
400+ self.c_creator.addIllustration((<IllustrationInfo>size_or_info).c_info, _content)
401+ elif isinstance(size_or_info, int):
402+ self.c_creator.addIllustration(<int>size_or_info, _content)
403+ else:
404+ raise TypeError(f"First argument must be int or IllustrationInfo, not {type(size_or_info)}")
390405
391406# def set_uuid(self, uuid) -> Creator:
392407# self.c_creator.setUuid(uuid)
@@ -819,6 +834,122 @@ cdef class ReadingBlob:
819834 self.view_count -= 1
820835
821836
837+ cdef class IllustrationInfo:
838+ """ Information about an illustration in a ZIM archive.
839+
840+ Attributes:
841+ width (int ): Width of the illustration in CSS pixels.
842+ height (int ): Height of the illustration in CSS pixels.
843+ scale (float ): Device pixel ratio (scale) of the illustration.
844+ extra_attributes (dict ): Additional attributes as key- value pairs.
845+ """
846+ __module__ = reader_module_name
847+ cdef zim.IllustrationInfo c_info
848+
849+ def __cinit__(self, width: pyint = 0, height: pyint = 0, scale: float = 1.0):
850+ """ Create an IllustrationInfo.
851+
852+ Args:
853+ width: Width of the illustration in CSS pixels.
854+ height: Height of the illustration in CSS pixels.
855+ scale: Device pixel ratio (default: 1.0 ).
856+ """
857+ if width or height:
858+ self.c_info = move(zim.IllustrationInfo(width, height, scale))
859+
860+ @staticmethod
861+ cdef from_illustration_info(zim.IllustrationInfo info):
862+ """ Creates a Python IllustrationInfo from a C++ IllustrationInfo.
863+
864+ Args:
865+ info: A C++ IllustrationInfo
866+
867+ Returns:
868+ IllustrationInfo: Casted illustration info
869+ """
870+ cdef IllustrationInfo ii = IllustrationInfo()
871+ ii.c_info = move(info)
872+ return ii
873+
874+ @staticmethod
875+ def from_metadata_item_name(name: str) -> IllustrationInfo:
876+ """ Parse an illustration metadata item name into IllustrationInfo.
877+
878+ Args:
879+ name: The metadata item name (e.g., " Illustration_48x48@2" ).
880+
881+ Returns:
882+ The parsed IllustrationInfo.
883+
884+ Raises:
885+ RuntimeError : If the name cannot be parsed.
886+ """
887+ cdef string _name = name.encode('UTF-8')
888+ cdef zim.IllustrationInfo info = zim.IllustrationInfo.fromMetadataItemName(_name)
889+ return IllustrationInfo.from_illustration_info(move(info))
890+
891+ @property
892+ def width(self) -> pyint:
893+ """ Width of the illustration in CSS pixels."""
894+ return self.c_info.width
895+
896+ @width.setter
897+ def width(self, value: pyint):
898+ self.c_info.width = value
899+
900+ @property
901+ def height(self) -> pyint:
902+ """ Height of the illustration in CSS pixels."""
903+ return self.c_info.height
904+
905+ @height.setter
906+ def height(self, value: pyint):
907+ self.c_info.height = value
908+
909+ @property
910+ def scale(self) -> float:
911+ """ Device pixel ratio (scale) of the illustration."""
912+ return self.c_info.scale
913+
914+ @scale.setter
915+ def scale(self, value: float):
916+ self.c_info.scale = value
917+
918+ @property
919+ def extra_attributes(self) -> Dict[str, str]:
920+ """ Additional attributes as key- value pairs."""
921+ result = {}
922+ for item in self.c_info.extraAttributes:
923+ result[item.first.decode('UTF-8')] = item.second.decode('UTF-8')
924+ return result
925+
926+ @extra_attributes.setter
927+ def extra_attributes(self, value: Dict[str, str]):
928+ """ Set additional attributes."""
929+ self.c_info.extraAttributes.clear()
930+ for key, val in value.items():
931+ self.c_info.extraAttributes[key.encode('UTF-8')] = val.encode('UTF-8')
932+
933+ def as_metadata_item_name(self) -> str:
934+ """ Convert this IllustrationInfo to a metadata item name.
935+
936+ Returns:
937+ The metadata item name (e.g., " Illustration_48x48@2" ).
938+ """
939+ return self.c_info.asMetadataItemName().decode('UTF-8')
940+
941+ def __repr__(self) -> str:
942+ return f"IllustrationInfo(width={self.width}, height={self.height}, scale={self.scale})"
943+
944+ def __eq__(self, other) -> pybool:
945+ if not isinstance(other, IllustrationInfo):
946+ return False
947+ return (self.width == other.width and
948+ self.height == other.height and
949+ self.scale == other.scale and
950+ self.extra_attributes == other.extra_attributes)
951+
952+
822953cdef class Entry:
823954 """ Entry in a ZIM archive.
824955
@@ -1305,9 +1436,18 @@ cdef class Archive:
13051436 def get_illustration_sizes(self) -> Set[pyint]:
13061437 """ Sizes for which an illustration is available (@1 scale only).
13071438
1439+ .. deprecated:: 3.8 .0
1440+ Use :meth:`get_illustration_infos` instead for full illustration metadata
1441+ including width, height, and scale information.
1442+
13081443 Returns:
13091444 The set of available sizes of the illustration.
13101445 """
1446+ warnings.warn(
1447+ "get_illustration_sizes() is deprecated, use get_illustration_infos() instead",
1448+ DeprecationWarning,
1449+ stacklevel=2
1450+ )
13111451 return self.c_archive.getIllustrationSizes()
13121452
13131453 def has_illustration(self, size: pyint = None) -> pybool:
@@ -1320,19 +1460,58 @@ cdef class Archive:
13201460 return self.c_archive.hasIllustration(size)
13211461 return self.c_archive.hasIllustration()
13221462
1323- def get_illustration_item(self, size: pyint = None) -> Item:
1463+ def get_illustration_item(self, size: pyint = None, info: IllustrationInfo = None ) -> Item:
13241464 """ Get the illustration Metadata item of the archive.
13251465
1466+ Args:
1467+ size: Optional size of the illustration (for backward compatibility).
1468+ info: Optional IllustrationInfo with width, height, and scale.
1469+
13261470 Returns:
13271471 The illustration item.
1472+
1473+ Note:
1474+ Either provide size (int ) or info (IllustrationInfo), not both.
1475+ If neither is provided, returns the default illustration item.
13281476 """
13291477 try:
1330- if size is not None:
1478+ if info is not None:
1479+ return Item.from_item(move(self.c_archive.getIllustrationItem(info.c_info)))
1480+ elif size is not None:
13311481 return Item.from_item(move(self.c_archive.getIllustrationItem(size)))
13321482 return Item.from_item(move(self.c_archive.getIllustrationItem()))
13331483 except RuntimeError as e:
13341484 raise KeyError(str(e))
13351485
1486+ def get_illustration_infos(self, width: pyint = None, height: pyint = None,
1487+ min_scale: float = None) -> List[IllustrationInfo]:
1488+ """ Get information about available illustrations.
1489+
1490+ Args:
1491+ width: Optional width to filter illustrations (must be provided with height).
1492+ height: Optional height to filter illustrations (must be provided with width).
1493+ min_scale: Optional minimum scale to filter illustrations (requires width and height).
1494+
1495+ Returns:
1496+ List of IllustrationInfo objects describing available illustrations.
1497+
1498+ Note:
1499+ - When called without arguments, returns all available illustrations.
1500+ - When called with width, height, and min_scale, filters illustrations.
1501+ """
1502+ cdef zim.IllustrationInfos infos
1503+ if width is not None and height is not None and min_scale is not None:
1504+ infos = self.c_archive.getIllustrationInfos(width, height, min_scale)
1505+ elif width is None and height is None and min_scale is None:
1506+ infos = self.c_archive.getIllustrationInfos()
1507+ else:
1508+ raise ValueError("Either provide all of (width, height, min_scale) or none of them")
1509+
1510+ result = []
1511+ for info in infos:
1512+ result.append(IllustrationInfo.from_illustration_info(info))
1513+ return result
1514+
13361515 @property
13371516 def cluster_cache_max_size(self) -> pyint:
13381517 """ Maximum size of the cluster cache.
@@ -1443,6 +1622,7 @@ reader_public_objects = [
14431622 Archive,
14441623 Entry,
14451624 Item,
1625+ IllustrationInfo,
14461626]
14471627reader = create_module(reader_module_name, reader_module_doc, reader_public_objects)
14481628
0 commit comments