from __future__ import annotations
from typing import TYPE_CHECKING, List, Dict, Tuple, Optional
import os
import re
import magic
import requests
from pathlib import Path
if TYPE_CHECKING:
from libacbf import ACBFBook
import libacbf.helpers as helpers
import libacbf.constants as consts
from libacbf.archivereader import ArchiveReader
from libacbf.bookdata import BookData
[docs]class Page:
r"""A page in the book.
See Also
--------
`Page Definition <https://acbf.fandom.com/wiki/Body_Section_Definition#Page>`_.
Attributes
----------
image_ref : str
Reference to the image file. May be embedded in the ACBF file, in the ACBF archive, in an external archive,
a local path or a URL.
There are several ways to format it to read data:
Reference to a file embedded in :class:`ACBFBook.data <libacbf.libacbf.ACBFData>`:
- ``“#page1.jpg“``
Reference to a file on disk:
- ``“/path/to/file/page1.jpg“``
- ``"C:\path\to\file\page1.jpg"``
- ``“file:///path/to/file/page1.jpg“``
- ``“file://C:\path\to\file\page1.jpg“``
Path to a file in the book's archive or relative path to file on disk if book is a plain ACBF XML:
- ``“page1.jpg“``
- ``“images/page1.jpg“``
Reference to file in an archive:
- ``“zip:path/to/archive.zip!/path/to/file/page1.jpg“``
URL address containing the image:
- ``“https://example.com/book1/images/page1.jpg“``
ref_type : ImageRefType(Enum)
A value from :class:`ImageRefType <libacbf.constants.ImageRefType>` indicating the type of reference in
:attr:`Page.image_ref`.
text_layers: Dict[str, TextLayer]
A dictionary with keys being the language of the text layer and values being :class:`TextLayer` objects.
frames: List[Frame]
A list of :class:`Frame` objects in order of appearance.
jumps: List[Jump]
A list of :class:`Jump` objects.
Warnings
--------
The attributes ``title``, ``bgcolor`` and ``transition`` are not available on
:attr:`ACBFBook.book_info.coverpage <libacbf.libacbf.BookInfo.coverpage>`.
Attributes
----------
title : Dict[str, str], optional
It is used to define beginning of chapters, sections of the book and can be used to create a table of contents.
Keys are standard language codes or ``'_'`` if not defined. Values are titles as string.
bgcolor : str, optional
Defines the background colour for the page. Inherits from
:attr:`ACBFBody.bgcolor <libacbf.libacbf.ACBFBody.bgcolor>` if ``None``.
transition: PageTransitions(Enum), optional
Defines the type of transition from the previous page to this one. Allowed values are in
:class:`PageTransitions <libacbf.constants.PageTransitions>`.
"""
def __init__(self, image_ref: str, book: ACBFBook, coverpage: bool = False):
self._book = book # Required to get embedded and archived images in `image` property
self._arch_path = None
self._file_path = None
self._file_id = None
self._image = None
self.is_coverpage: bool = coverpage
self.ref_type: consts.ImageRefType = None
self.image_ref = image_ref # Set property
self.text_layers: Dict[str, TextLayer] = {}
self.frames: List[Frame] = []
self.jumps: List[Jump] = []
# --- Optional ---
if not coverpage:
self.bgcolor: Optional[str] = None
self.transition: Optional[consts.PageTransitions] = None
self.title: Dict[str, str] = {}
def __repr__(self):
if self.is_coverpage:
return f'<libacbf.BookInfo.coverpage as `Page` href="{self.image_ref}">'
else:
return f'<libacbf.body.Page href="{self.image_ref}">'
@property
def image_ref(self) -> str:
return self._image_ref
@image_ref.setter
def image_ref(self, ref: str):
self._image = None
if ref.startswith('#'):
self.ref_type = consts.ImageRefType.Embedded
self._file_id = re.sub('#', '', ref)
elif ref.startswith("zip:"):
self.ref_type = consts.ImageRefType.Archived
ref_path = re.sub("zip:", '', ref)
self._arch_path = Path(re.split("!", ref_path)[0])
self._file_path = Path(re.split("!", ref_path)[1])
self._file_id = self._file_path.name
if not os.path.isabs(self._arch_path):
self._arch_path = Path(os.path.abspath(str(self._arch_path)))
elif re.fullmatch(helpers.url_pattern, ref):
self.ref_type = consts.ImageRefType.URL
self._file_id = re.split("/", ref)[-1]
else:
if ref.startswith("file://"):
self._file_path = Path(os.path.abspath(ref))
else:
self._file_path = Path(ref)
if os.path.isabs(ref):
self.ref_type = consts.ImageRefType.Local
else:
if self._book.archive is not None:
self.ref_type = consts.ImageRefType.SelfArchived
else:
self.ref_type = consts.ImageRefType.Local
self._file_path = self._book.book_path.parent / self._file_path
self._file_id = self._file_path.name
self._image_ref: str = ref
@property
def image(self) -> BookData:
"""Gets the image data from the source.
Returns
-------
BookData
A :class:`BookData <libacbf.bookdata.BookData>` object.
"""
if self._image is None:
if self.ref_type == consts.ImageRefType.Embedded:
self._image = self._book.data[self._file_id]
return self._image
elif self.ref_type == consts.ImageRefType.Archived:
with ArchiveReader(self._arch_path) as ext_archive:
contents = ext_archive.read(str(self._file_path))
elif self.ref_type == consts.ImageRefType.URL:
response = requests.get(self.image_ref)
contents = response.content
else:
if self.ref_type == consts.ImageRefType.SelfArchived:
contents = self._book.archive.read(str(self._file_path))
elif self.ref_type == consts.ImageRefType.Local:
with open(str(self._file_path), "rb") as image:
contents = image.read()
contents_type = magic.from_buffer(contents, True)
self._image = BookData(self._file_id, contents_type, contents)
return self._image
[docs] @helpers.check_book
def set_transition(self, tr: Optional[str]):
"""Set transition by string.
Parameters
----------
tr : str | None
Transition value to be set. Pass ``None`` to remove.
"""
if self.is_coverpage:
raise AttributeError("`coverpage` has no attribute `transition`.")
self.transition = consts.PageTransitions[tr] if tr is not None else tr
[docs] @helpers.check_book
def add_textlayer(self, lang: str, *areas: TextArea) -> TextLayer:
"""Add a text layer to the page.
Parameters
----------
lang : str
The language of the text layer.
*areas : TextArea, optional
TextArea objects to fill the layer with.
Returns
-------
TextLayer
The newly created text layer.
"""
tl = TextLayer(*areas)
self.text_layers[lang] = tl
return tl
[docs] @helpers.check_book
def insert_frame(self, index: int, points: List[Tuple[int, int]]) -> Frame:
"""Insert a frame at the index.
Parameters
----------
index : int
Index to insert at.
points : List[Tuple[int, int]]
The points defining the frame.
Returns
-------
Frame
The newly created frame.
"""
fr = Frame(points)
self.frames.insert(index, fr)
return fr
[docs] @helpers.check_book
def append_frame(self, points: List[Tuple[int, int]]) -> Frame:
"""Append a frame to the page.
Returns
-------
Frame
The newly created frame.
"""
fr = Frame(points)
self.frames.append(fr)
return fr
[docs] @helpers.check_book
def add_jump(self, target: int, points: List[Tuple[int, int]]) -> Jump:
"""Add a jump to the page.
Parameters
----------
target : int
The target page. ``0`` is the cover page, ``1`` is the first page, ``2`` is the second page etc.
points : List[Tuple[int, int]]
The points defining the jump.
Returns
-------
Jump
The newly created jump.
"""
jp = Jump(target, points, self._book)
self.jumps.append(jp)
return jp
[docs]class TextLayer:
"""Defines a text layer drawn on a page.
See Also
--------
`Text Layer specifications <https://acbf.fandom.com/wiki/Body_Section_Definition#Text-layer>`_.
Attributes
----------
text_areas : List[TextArea]
A list of :class:`TextArea` objects in order (order matters for text-to-speech).
bgcolor : str, optional
Defines the background colour of the text areas or inherits from :attr:`Page.bgcolor` if ``None``.
"""
def __init__(self, *areas: TextArea):
self.text_areas: List[TextArea] = list(areas)
self.bgcolor: Optional[str] = None
[docs] def insert_textarea(self, index: int, text: str, points: List[Tuple[int, int]]) -> TextArea:
"""Insert a text area at the index.
Parameters
----------
index : int
Index to insert at.
text : str
Multiline text of the text area.
points : List[Tuple[int, int]]
The points that define the text area.
Returns
-------
The newly created text area.
"""
ta = TextArea(text, points)
self.text_areas.insert(index, ta)
return ta
[docs] def append_textarea(self, text: str, points: List[Tuple[int, int]]) -> TextArea:
"""Append a text area to the layer.
Parameters
----------
text : str
Multiline text of the text area.
points : List[Tuple[int, int]]
The points that define the text area.
Returns
-------
The newly created text area.
"""
ta = TextArea(text, points)
self.text_areas.append(ta)
return ta
[docs]class TextArea:
"""Defines an area where text is drawn.
See Also
--------
`Text Area specifications <https://acbf.fandom.com/wiki/Body_Section_Definition#Text-area>`_.
Attributes
----------
points : List[Tuple[int, int]]
A list of tuples as coordinates.
text : str
A multiline string of what text to show in the are. Can have special tags for formatting.
<strong>...</strong>
Bold letters.
<emphasis>...</emphasis>
Italicised or cursive text.
<strikethrough>...</strikethrough>
Striked-through text.
<sub>...</sub>
Subscript text.
<sup>...</sup>
Superscript text.
<a href=“...“>...</a>
A link. Internal or external.
bgcolor : str, optional
Defines the background colour of the text area or inherits from :attr:`TextLayer.bgcolor` if ``None``.
rotation : int, optional
Defines the rotation of the text layer.
Can be an integer from 0 to 360.
type : TextAreas(Enum), optional
The type of text area. Rendering can be changed based on type. Allowed values are defined in
:class:`TextAreas <libacbf.constants.TextAreas>`.
inverted : bool, optional
Whether text is rendered with inverted colour.
transparent : bool, optional
Whether text is drawn.
"""
def __init__(self, text: str, points: List[Tuple[int, int]]):
self.text: str = text
self.points: List[Tuple[int, int]] = points
# --- Optional ---
self.bgcolor: Optional[str] = None
self.rotation: Optional[int] = None
self.type: Optional[consts.TextAreas] = None
self.inverted: Optional[bool] = None
self.transparent: Optional[bool] = None
[docs] def set_type(self, ty: Optional[str]):
"""Set type by string.
Parameters
----------
ty : str | None
Type to set or ``None`` to remove.
"""
self.type = consts.TextAreas[ty] if ty is not None else ty
[docs]class Frame:
"""A subsection of a page.
See Also
--------
`Frame specifications <https://acbf.fandom.com/wiki/Body_Section_Definition#Frame>`_.
Attributes
----------
points : List[Tuple[int, int]]
A list of tuples as coordinates.
bgcolor : str, optional
Defines the background colour for the page. Inherits from :attr:`Page.bgcolor <libacbf.body.Page.bgcolor>` if
``None``.
"""
def __init__(self, points: List[Tuple[int, int]]):
self.points: List[Tuple[int, int]] = points
self.bgcolor: Optional[str] = None
[docs]class Jump:
"""Clickable area on a page which navigates to another page.
See Also
--------
`body Info Jump specifications <https://acbf.fandom.com/wiki/Body_Section_Definition#Jump>`_.
Attributes
----------
target : int
The target page index. Cover page is ``0``, first page is ``1``, second page is ``2`` and so on.
points : List[Tuple[int, int]]
A list of tuples as coordinates.
"""
def __init__(self, target: int, points: List[Tuple[int, int]], book: ACBFBook):
self._book = book
self.target = target
self.points: List[Tuple[int, int]] = points
@property
def page(self) -> Page:
"""Target page to go to when clicked.
"""
if self.target == 0:
return self._book.book_info.coverpage
else:
return self._book.body.pages[self.target]