Skip to content

Commit 4da244a

Browse files
committed
Add support for new illustrations APIs in libzim 9.4.0
1 parent 1004cf3 commit 4da244a

File tree

15 files changed

+812
-23
lines changed

15 files changed

+812
-23
lines changed

.github/workflows/CI-wheels.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ on:
77
- main
88

99
env:
10-
LIBZIM_DL_VERSION: "9.3.0-1"
10+
LIBZIM_DL_VERSION: "9.4.0"
1111
MACOSX_DEPLOYMENT_TARGET: "13.0"
1212
CIBW_ENVIRONMENT_PASS_LINUX: "LIBZIM_DL_VERSION"
1313
CIBW_BUILD_VERBOSITY: "3"

.github/workflows/Publish.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ on:
66
- published
77

88
env:
9-
LIBZIM_DL_VERSION: "9.3.0-1"
9+
LIBZIM_DL_VERSION: "9.4.0"
1010
MACOSX_DEPLOYMENT_TARGET: "13.0"
1111
CIBW_ENVIRONMENT_PASS_LINUX: "LIBZIM_DL_VERSION"
1212
# APPLE_SIGNING_KEYCHAIN_PATH set in prepare keychain step

.github/workflows/QA.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: QA
22
on: [push]
33

44
env:
5-
LIBZIM_DL_VERSION: "9.3.0-1"
5+
LIBZIM_DL_VERSION: "9.4.0"
66
MACOSX_DEPLOYMENT_TARGET: "13.0"
77

88
jobs:

.github/workflows/Tests.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ name: Tests
22
on: [push]
33

44
env:
5-
LIBZIM_DL_VERSION: "9.3.0-1"
5+
LIBZIM_DL_VERSION: "9.4.0"
66
MACOSX_DEPLOYMENT_TARGET: "13.0"
77
# we want cython traces for coverage
88
PROFILE: "1"

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- libzim 9.4.0 new illustration APIs (#232)
13+
1014
## [3.7.0] - 2025-04-18
1115

1216
### Added

libzim/libwrapper.h

Lines changed: 13 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>
@@ -106,6 +107,17 @@ class Blob : public Wrapper<zim::Blob>
106107
FORWARD(zim::size_type, size)
107108
};
108109

110+
class IllustrationInfo : public Wrapper<zim::IllustrationInfo>
111+
{
112+
public:
113+
IllustrationInfo() = default;
114+
IllustrationInfo(const zim::IllustrationInfo& o) : Wrapper(o) {}
115+
IllustrationInfo(uint32_t width, uint32_t height, float scale) : Wrapper(zim::IllustrationInfo{width, height, scale}) {}
116+
zim::IllustrationInfo& operator*() const { return *mp_base; }
117+
operator zim::IllustrationInfo() const { return *mp_base; }
118+
FORWARD(std::string, asMetadataItemName)
119+
};
120+
109121
class Item : public Wrapper<zim::Item>
110122
{
111123
public:
@@ -147,6 +159,7 @@ class Archive : public Wrapper<zim::Archive>
147159
FORWARD(wrapper::Entry, getRandomEntry)
148160
FORWARD(wrapper::Item, getIllustrationItem)
149161
FORWARD(std::set<unsigned int>, getIllustrationSizes)
162+
FORWARD(zim::IllustrationInfos, getIllustrationInfos)
150163
std::string getUuid() const
151164
{ auto u = mp_base->getUuid();
152165
std::string uuids(u.data, u.size());

libzim/libzim.pyx

Lines changed: 186 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import os
3939
import pathlib
4040
import sys
4141
import traceback
42+
import warnings
4243
from collections import OrderedDict
4344
from types import ModuleType
4445
from 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+
822953
cdef 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
]
14471627
reader = create_module(reader_module_name, reader_module_doc, reader_public_objects)
14481628

libzim/reader.pyi

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@ from __future__ import annotations
22

33
import pathlib
44
from uuid import UUID
5+
from typing import overload
6+
7+
class IllustrationInfo:
8+
def __init__(self, width: int = 0, height: int = 0, scale: float = 1.0) -> None: ...
9+
@staticmethod
10+
def from_metadata_item_name(name: str) -> IllustrationInfo: ...
11+
@property
12+
def width(self) -> int: ...
13+
@width.setter
14+
def width(self, value: int) -> None: ...
15+
@property
16+
def height(self) -> int: ...
17+
@height.setter
18+
def height(self, value: int) -> None: ...
19+
@property
20+
def scale(self) -> float: ...
21+
@scale.setter
22+
def scale(self, value: float) -> None: ...
23+
@property
24+
def extra_attributes(self) -> dict[str, str]: ...
25+
@extra_attributes.setter
26+
def extra_attributes(self, value: dict[str, str]) -> None: ...
27+
def as_metadata_item_name(self) -> str: ...
28+
def __repr__(self) -> str: ...
29+
def __eq__(self, other: object) -> bool: ...
530

631
class Item:
732
@property
@@ -76,7 +101,14 @@ class Archive:
76101
def media_count(self) -> int: ...
77102
def get_illustration_sizes(self) -> set[int]: ...
78103
def has_illustration(self, size: int | None = None) -> bool: ...
79-
def get_illustration_item(self, size: int | None = None) -> Item: ...
104+
@overload
105+
def get_illustration_item(self, size: int | None = None, info: None = None) -> Item: ...
106+
@overload
107+
def get_illustration_item(self, size: None = None, info: IllustrationInfo | None = None) -> Item: ...
108+
@overload
109+
def get_illustration_infos(self) -> list[IllustrationInfo]: ...
110+
@overload
111+
def get_illustration_infos(self, width: int, height: int, min_scale: float) -> list[IllustrationInfo]: ...
80112
@property
81113
def cluster_cache_max_size(self) -> int: ...
82114
@cluster_cache_max_size.setter

0 commit comments

Comments
 (0)