@@ -479,20 +479,34 @@ cdef class _Creator:
479479 self.c_creator.setMainPath(mainPath.encode('UTF-8'))
480480 return self
481481
482- def add_illustration(self, int size: pyint , content: bytes):
482+ def add_illustration(self, size_or_info , content: bytes):
483483 """ Add a PNG illustration to Archive.
484484
485485 Refer to https:// wiki.openzim.org/ wiki/ Metadata for more details.
486486
487487 Args:
488- size (int ): The width of the square PNG illustration in pixels.
488+ size_or_info: Either an int (width of the square PNG illustration in pixels)
489+ or an IllustrationInfo object with width, height, and scale.
489490 content (bytes): The binary content of the PNG illustration.
490491
491492 Raises:
492- RuntimeError : If an illustration with the same width already exists.
493+ RuntimeError : If an illustration with the same attributes already exists.
494+
495+ Examples:
496+ # Old style (square illustration at scale 1)
497+ creator.add_illustration(48 , png_data)
498+
499+ # New style (with dimensions and scale)
500+ info = IllustrationInfo(48 , 48 , 2.0 )
501+ creator.add_illustration(info, png_data)
493502 """
494503 cdef string _content = content
495- self.c_creator.addIllustration(size, _content)
504+ if isinstance(size_or_info, IllustrationInfo):
505+ self.c_creator.addIllustration((<IllustrationInfo>size_or_info).c_info, _content)
506+ elif isinstance(size_or_info, int):
507+ self.c_creator.addIllustration(<int>size_or_info, _content)
508+ else:
509+ raise TypeError(f"First argument must be int or IllustrationInfo, not {type(size_or_info)}")
496510
497511# def set_uuid(self, uuid) -> _Creator:
498512# self.c_creator.setUuid(uuid)
@@ -762,6 +776,146 @@ writer_public_objects = [
762776writer = create_module(writer_module_name, writer_module_doc, writer_public_objects)
763777
764778
779+ ###############################################################################
780+ # Illustration module #
781+ ###############################################################################
782+
783+ illustration_module_name = f"{__name__}.illustration"
784+
785+ cdef class IllustrationInfo:
786+ """ Information about an illustration in a ZIM archive.
787+
788+ Attributes:
789+ width (int ): Width of the illustration in CSS pixels.
790+ height (int ): Height of the illustration in CSS pixels.
791+ scale (float ): Device pixel ratio (scale) of the illustration.
792+ extra_attributes (dict ): Additional attributes as key- value pairs.
793+ """
794+ __module__ = illustration_module_name
795+ cdef zim.IllustrationInfo c_info
796+ def __cinit__(self, width: pyint = 0, height: pyint = 0, scale: float = 1.0, extra_attributes: Dict[str, str] = None):
797+ """ Create an IllustrationInfo.
798+
799+ Args:
800+ width: Width of the illustration in CSS pixels.
801+ height: Height of the illustration in CSS pixels.
802+ scale: Device pixel ratio (default: 1.0 ).
803+ extra_attributes: Additional attributes as key- value pairs (optional).
804+ """
805+ # Initialize struct fields directly
806+ self.c_info.width = width
807+ self.c_info.height = height
808+ self.c_info.scale = scale
809+ self.c_info.extraAttributes = zim.Attributes({})
810+
811+ # Set extra attributes if provided (need to encode strings to bytes)
812+ if extra_attributes is not None:
813+ for key, val in extra_attributes.items():
814+ self.c_info.extraAttributes[key.encode('UTF-8')] = val.encode('UTF-8')
815+
816+ @staticmethod
817+ cdef from_illustration_info(zim.IllustrationInfo info):
818+ """ Creates a Python IllustrationInfo from a C++ IllustrationInfo.
819+
820+ Args:
821+ info: A C++ IllustrationInfo
822+
823+ Returns:
824+ IllustrationInfo: Casted illustration info
825+ """
826+ cdef IllustrationInfo ii = IllustrationInfo()
827+ ii.c_info = move(info)
828+ return ii
829+ @staticmethod
830+ def from_metadata_item_name(name: str) -> IllustrationInfo:
831+ """ Parse an illustration metadata item name into IllustrationInfo.
832+
833+ Args:
834+ name: The metadata item name (e.g., " Illustration_48x48@2" ).
835+
836+ Returns:
837+ The parsed IllustrationInfo.
838+
839+ Raises:
840+ RuntimeError : If the name cannot be parsed.
841+ """
842+ cdef string _name = name.encode('UTF-8')
843+ cdef zim.IllustrationInfo info = zim.IllustrationInfo.fromMetadataItemName(_name)
844+ return IllustrationInfo.from_illustration_info(move(info))
845+ @property
846+ def width(self) -> pyint:
847+ """ Width of the illustration in CSS pixels."""
848+ return self.c_info.width
849+ @width.setter
850+ def width(self, value: pyint):
851+ self.c_info.width = value
852+ @property
853+ def height(self) -> pyint:
854+ """ Height of the illustration in CSS pixels."""
855+ return self.c_info.height
856+ @height.setter
857+ def height(self, value: pyint):
858+ self.c_info.height = value
859+ @property
860+ def scale(self) -> float:
861+ """ Device pixel ratio (scale) of the illustration."""
862+ return self.c_info.scale
863+ @scale.setter
864+ def scale(self, value: float):
865+ self.c_info.scale = value
866+ @property
867+ def extra_attributes(self) -> Dict[str, str]:
868+ """ Additional attributes as key- value pairs."""
869+ result = {}
870+ for item in self.c_info.extraAttributes:
871+ result[item.first.decode('UTF-8')] = item.second.decode('UTF-8')
872+ return result
873+ @extra_attributes.setter
874+ def extra_attributes(self, value: Dict[str, str]):
875+ """ Set additional attributes."""
876+ self.c_info.extraAttributes.clear()
877+ for key, val in value.items():
878+ self.c_info.extraAttributes[key.encode('UTF-8')] = val.encode('UTF-8')
879+ def as_metadata_item_name(self) -> str:
880+ """ Convert this IllustrationInfo to a metadata item name.
881+
882+ Returns:
883+ The metadata item name (e.g., " Illustration_48x48@2" ).
884+ """
885+ return self.c_info.asMetadataItemName().decode('UTF-8')
886+ def __repr__(self) -> str:
887+ return f"IllustrationInfo(width={self.width}, height={self.height}, scale={self.scale})"
888+ def __eq__(self, other) -> pybool:
889+ if not isinstance(other, IllustrationInfo):
890+ return False
891+ return (self.width == other.width and
892+ self.height == other.height and
893+ self.scale == other.scale and
894+ self.extra_attributes == other.extra_attributes)
895+
896+
897+ illustration_module_doc = """ Illustration data structures for ZIM archives
898+
899+ This module provides classes for working with illustrations in ZIM archives.
900+
901+ Usage:
902+
903+ ```python
904+ from libzim.illustration import IllustrationInfo
905+
906+ # Create an IllustrationInfo
907+ info = IllustrationInfo(48 , 48 , 2.0 )
908+ print (f" Metadata name: {info.as_metadata_item_name()}" )
909+
910+ # Parse from metadata name
911+ parsed = IllustrationInfo.from_metadata_item_name(" Illustration_48x48@2" )
912+ ```"""
913+ illustration_public_objects = [
914+ IllustrationInfo,
915+ ]
916+ illustration = create_module(illustration_module_name, illustration_module_doc, illustration_public_objects)
917+
918+
765919###############################################################################
766920# Reader module #
767921###############################################################################
@@ -1329,19 +1483,57 @@ cdef class Archive:
13291483 return self.c_archive.hasIllustration(size)
13301484 return self.c_archive.hasIllustration()
13311485
1332- def get_illustration_item(self, size: pyint = None) -> Item:
1486+ def get_illustration_item(self, size: pyint = None, info: IllustrationInfo = None ) -> Item:
13331487 """ Get the illustration Metadata item of the archive.
13341488
1489+ Args:
1490+ size: Optional size of the illustration (for backward compatibility).
1491+ info: Optional IllustrationInfo with width, height, and scale.
1492+
13351493 Returns:
13361494 The illustration item.
1495+
1496+ Note:
1497+ Either provide size (int ) or info (IllustrationInfo), not both.
1498+ If neither is provided, returns the default illustration item.
13371499 """
13381500 try:
1339- if size is not None:
1340- return Item.from_item(move(self.c_archive.getIllustrationItem(size)))
1501+ if info is not None:
1502+ return Item.from_item(move(self.c_archive.getIllustrationItem(info.c_info)))
1503+ elif size is not None:
1504+ return Item.from_item(move(self.c_archive.getIllustrationItem(<int>size)))
13411505 return Item.from_item(move(self.c_archive.getIllustrationItem()))
13421506 except RuntimeError as e:
13431507 raise KeyError(str(e))
13441508
1509+ def get_illustration_infos(self, width: pyint = None, height: pyint = None,
1510+ min_scale: float = None) -> List[IllustrationInfo]:
1511+ """ Get information about available illustrations.
1512+
1513+ Args:
1514+ width: Optional width to filter illustrations (must be provided with height).
1515+ height: Optional height to filter illustrations (must be provided with width).
1516+ min_scale: Optional minimum scale to filter illustrations (requires width and height).
1517+
1518+ Returns:
1519+ List of IllustrationInfo objects describing available illustrations.
1520+
1521+ Note:
1522+ - When called without arguments, returns all available illustrations.
1523+ - When called with width, height, and min_scale, filters illustrations.
1524+ """
1525+ cdef zim.Archive.IllustrationInfos infos
1526+ if width is not None and height is not None and min_scale is not None:
1527+ infos = self.c_archive.getIllustrationInfos(width, height, min_scale)
1528+ elif width is None and height is None and min_scale is None:
1529+ infos = self.c_archive.getIllustrationInfos()
1530+ else:
1531+ raise ValueError("Either provide all of (width, height, min_scale) or none of them")
1532+ result = []
1533+ for info in infos:
1534+ result.append(IllustrationInfo.from_illustration_info(info))
1535+ return result
1536+
13451537 @property
13461538 def dirent_cache_max_size(self) -> pyint:
13471539 """ Maximum size of the dirent cache.
@@ -1380,7 +1572,7 @@ def get_cluster_cache_max_size() -> pyint:
13801572 """ Get the maximum size of the cluster cache.
13811573
13821574 Returns:
1383- (int ): the maximum memory size used by the cluster cache (in bytes).
1575+ (int ): the maximum memory size used by the cluster cache (in bytes).
13841576 """
13851577 return zim.getClusterCacheMaxSize()
13861578
@@ -1400,7 +1592,7 @@ def get_cluster_cache_current_size() -> pyint:
14001592 """ Get the current size of the cluster cache.
14011593
14021594 Returns:
1403- (int ): the current memory size (in bytes) used by the cluster cache.
1595+ (int ): the current memory size (in bytes) used by the cluster cache.
14041596 """
14051597 return zim.getClusterCacheCurrentSize()
14061598
@@ -1743,6 +1935,7 @@ class ModuleLoader(importlib.abc.Loader):
17431935 @staticmethod
17441936 def create_module(spec):
17451937 return {
1938+ 'libzim.illustration': illustration,
17461939 'libzim.writer': writer,
17471940 'libzim.reader': reader,
17481941 'libzim.search': search,
@@ -1766,4 +1959,4 @@ class ModuleFinder(importlib.abc.MetaPathFinder):
17661959# register finder for our submodules
17671960sys.meta_path.insert(0, ModuleFinder())
17681961
1769- __all__ = ["writer", "reader", "search", "suggestion", "version"]
1962+ __all__ = ["illustration", " writer", "reader", "search", "suggestion", "version"]
0 commit comments