Skip to content

Commit bfd945f

Browse files
committed
Add support for new illustrations APIs in libzim 9.4.0
1 parent 0f0820a commit bfd945f

File tree

11 files changed

+814
-21
lines changed

11 files changed

+814
-21
lines changed

libzim/__init__.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from libzim import (
2+
illustration, # noqa: F401 # pyright: ignore[reportUnusedImport]
23
reader, # noqa: F401 # pyright: ignore[reportUnusedImport]
34
search, # noqa: F401 # pyright: ignore[reportUnusedImport]
45
suggestion, # noqa: F401 # pyright: ignore[reportUnusedImport]

libzim/illustration.pyi

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from __future__ import annotations
2+
3+
class IllustrationInfo:
4+
"""Information about an illustration in a ZIM archive."""
5+
6+
def __init__(
7+
self,
8+
width: int = 0,
9+
height: int = 0,
10+
scale: float = 1.0,
11+
extra_attributes: dict[str, str] | None = None,
12+
) -> None: ...
13+
@staticmethod
14+
def from_metadata_item_name(name: str) -> IllustrationInfo: ...
15+
@property
16+
def width(self) -> int: ...
17+
@width.setter
18+
def width(self, value: int) -> None: ...
19+
@property
20+
def height(self) -> int: ...
21+
@height.setter
22+
def height(self, value: int) -> None: ...
23+
@property
24+
def scale(self) -> float: ...
25+
@scale.setter
26+
def scale(self, value: float) -> None: ...
27+
@property
28+
def extra_attributes(self) -> dict[str, str]: ...
29+
@extra_attributes.setter
30+
def extra_attributes(self, value: dict[str, str]) -> None: ...
31+
def as_metadata_item_name(self) -> str: ...
32+
def __repr__(self) -> str: ...
33+
def __eq__(self, other: object) -> bool: ...

libzim/libwrapper.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
#include <zim/archive.h>
2727
#include <zim/entry.h>
2828
#include <zim/item.h>
29+
#include <zim/illustration.h>
2930
#include <zim/writer/item.h>
3031
#include <zim/writer/contentProvider.h>
3132
#include <zim/search.h>
@@ -136,6 +137,8 @@ class Entry : public Wrapper<zim::Entry>
136137
class Archive : public Wrapper<zim::Archive>
137138
{
138139
public:
140+
typedef zim::Archive::IllustrationInfos IllustrationInfos;
141+
139142
Archive() = default;
140143
Archive(const std::string& filename) : Wrapper(zim::Archive(filename)) {};
141144
Archive(const zim::Archive& o) : Wrapper(o) {};
@@ -147,6 +150,7 @@ class Archive : public Wrapper<zim::Archive>
147150
FORWARD(wrapper::Entry, getRandomEntry)
148151
FORWARD(wrapper::Item, getIllustrationItem)
149152
FORWARD(std::set<unsigned int>, getIllustrationSizes)
153+
FORWARD(zim::Archive::IllustrationInfos, getIllustrationInfos)
150154
std::string getUuid() const
151155
{ auto u = mp_base->getUuid();
152156
std::string uuids(u.data, u.size());

libzim/libzim.pyx

Lines changed: 203 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -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 = [
762776
writer = 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
17671960
sys.meta_path.insert(0, ModuleFinder())
17681961
1769-
__all__ = ["writer", "reader", "search", "suggestion", "version"]
1962+
__all__ = ["illustration", "writer", "reader", "search", "suggestion", "version"]

libzim/reader.pyi

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
from __future__ import annotations
22

33
import pathlib
4+
from typing import overload
45
from uuid import UUID
56

7+
from libzim.illustration import IllustrationInfo
8+
69
class Item:
710
@property
811
def title(self) -> str: ...
@@ -76,7 +79,21 @@ class Archive:
7679
def media_count(self) -> int: ...
7780
def get_illustration_sizes(self) -> set[int]: ...
7881
def has_illustration(self, size: int | None = None) -> bool: ...
79-
def get_illustration_item(self, size: int | None = None) -> Item: ...
82+
@overload
83+
def get_illustration_item(self) -> Item: ...
84+
@overload
85+
def get_illustration_item(self, size: int) -> Item: ...
86+
@overload
87+
def get_illustration_item(self, *, info: IllustrationInfo) -> Item: ...
88+
@overload
89+
def get_illustration_infos(self) -> list[IllustrationInfo]: ...
90+
@overload
91+
def get_illustration_infos(
92+
self,
93+
width: int | None = None,
94+
height: int | None = None,
95+
min_scale: float | None = None,
96+
) -> list[IllustrationInfo]: ...
8097
@property
8198
def dirent_cache_max_size(self) -> int: ...
8299
@dirent_cache_max_size.setter

0 commit comments

Comments
 (0)