Skip to content

Commit

Permalink
ENH: Ease access to ViewerPreferences (#2144)
Browse files Browse the repository at this point in the history
Closes #2105
  • Loading branch information
pubpub-zz authored Sep 5, 2023
1 parent 455c773 commit 05f2a65
Show file tree
Hide file tree
Showing 5 changed files with 257 additions and 0 deletions.
14 changes: 14 additions & 0 deletions pypdf/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@
PdfObject,
TextStringObject,
TreeObject,
ViewerPreferences,
read_object,
)
from .types import OutlineType, PagemodeType
Expand Down Expand Up @@ -293,6 +294,19 @@ class PdfReader:
Defaults to ``None``
"""

@property
def viewer_preferences(self) -> Optional[ViewerPreferences]:
"""Returns the existing ViewerPreferences as a overloaded dictionniary."""
o = cast(DictionaryObject, self.trailer["/Root"]).get(
CD.VIEWER_PREFERENCES, None
)
if o is None:
return None
o = o.get_object()
if not isinstance(o, ViewerPreferences):
o = ViewerPreferences(o)
return o

def __init__(
self,
stream: Union[StrByteType, Path],
Expand Down
22 changes: 22 additions & 0 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@
TypFitArguments,
UserAccessPermissions,
)
from .constants import CatalogDictionary as CD
from .constants import Core as CO
from .constants import (
FieldDictionaryAttributes as FA,
Expand Down Expand Up @@ -110,6 +111,7 @@
StreamObject,
TextStringObject,
TreeObject,
ViewerPreferences,
create_string_object,
hex_to_rgb,
)
Expand Down Expand Up @@ -367,6 +369,26 @@ def set_need_appearances_writer(self, state: bool = True) -> None:
f"set_need_appearances_writer({state}) catch : {exc}", __name__
)

@property
def viewer_preferences(self) -> Optional[ViewerPreferences]:
"""Returns the existing ViewerPreferences as a overloaded dictionniary."""
o = cast(DictionaryObject, self._root_object).get(CD.VIEWER_PREFERENCES, None)
if o is None:
return None
o = o.get_object()
if not isinstance(o, ViewerPreferences):
o = ViewerPreferences(o)
if hasattr(o, "indirect_reference"):
self._replace_object(o.indirect_reference, o)
else:
self._root_object[NameObject(CD.VIEWER_PREFERENCES)] = o
return o

def create_viewer_preference(self) -> ViewerPreferences:
o = ViewerPreferences()
self._root_object[NameObject(CD.VIEWER_PREFERENCES)] = self._add_object(o)
return o

def add_page(
self,
page: PageObject,
Expand Down
2 changes: 2 additions & 0 deletions pypdf/generic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
read_hex_string_from_stream,
read_string_from_stream,
)
from ._viewerpref import ViewerPreferences


def readHexStringFromStream(
Expand Down Expand Up @@ -443,6 +444,7 @@ def link(
"RectangleObject",
"Field",
"Destination",
"ViewerPreferences",
# --- More specific stuff
# Outline
"OutlineItem",
Expand Down
154 changes: 154 additions & 0 deletions pypdf/generic/_viewerpref.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# Copyright (c) 2023, Pubpub-ZZ
#
# All rights reserved.
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions are
# met:
#
# * Redistributions of source code must retain the above copyright notice,
# this list of conditions and the following disclaimer.
# * Redistributions in binary form must reproduce the above copyright notice,
# this list of conditions and the following disclaimer in the documentation
# and/or other materials provided with the distribution.
# * The name of the author may not be used to endorse or promote products
# derived from this software without specific prior written permission.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.

from typing import (
Any,
List,
Optional,
)

from ._base import BooleanObject, NameObject, NumberObject
from ._data_structures import ArrayObject, DictionaryObject

f_obj = BooleanObject(False)


class ViewerPreferences(DictionaryObject):
def _get_bool(self, key: str, deft: Optional[BooleanObject]) -> BooleanObject:
return self.get(key, deft)

def _set_bool(self, key: str, v: bool) -> None:
self[NameObject(key)] = BooleanObject(v is True)

def _get_name(self, key: str, deft: Optional[NameObject]) -> Optional[NameObject]:
return self.get(key, deft)

def _set_name(self, key: str, lst: List[str], v: NameObject) -> None:
if v[0] != "/":
raise ValueError(f"{v} is not starting with '/'")
if lst != [] and v not in lst:
raise ValueError(f"{v} is not par of acceptable values")
self[NameObject(key)] = NameObject(v)

def _get_arr(self, key: str, deft: Optional[List[Any]]) -> NumberObject:
return self.get(key, None if deft is None else ArrayObject(deft))

def _set_arr(self, key: str, v: Optional[ArrayObject]) -> None:
if not isinstance(v, ArrayObject):
raise ValueError("ArrayObject is expected")
self[NameObject(key)] = v

def _get_int(self, key: str, deft: Optional[NumberObject]) -> NumberObject:
return self.get(key, deft)

def _set_int(self, key: str, v: int) -> None:
self[NameObject(key)] = NumberObject(v)

def __new__(cls: Any, value: Any = None) -> "ViewerPreferences":
def _add_prop_bool(key: str, deft: Optional[BooleanObject]) -> property:
return property(
lambda self: self._get_bool(key, deft),
lambda self, v: self._set_bool(key, v),
None,
f"""
Returns/Modify the status of {key}, Returns {deft} if not defined
""",
)

def _add_prop_name(
key: str, lst: List[str], deft: Optional[NameObject]
) -> property:
return property(
lambda self: self._get_name(key, deft),
lambda self, v: self._set_name(key, lst, v),
None,
f"""
Returns/Modify the status of {key}, Returns {deft} if not defined.
Acceptable values: {lst}
""",
)

def _add_prop_arr(key: str, deft: Optional[ArrayObject]) -> property:
return property(
lambda self: self._get_arr(key, deft),
lambda self, v: self._set_arr(key, v),
None,
f"""
Returns/Modify the status of {key}, Returns {deft} if not defined
""",
)

def _add_prop_int(key: str, deft: Optional[int]) -> property:
return property(
lambda self: self._get_int(key, deft),
lambda self, v: self._set_int(key, v),
None,
f"""
Returns/Modify the status of {key}, Returns {deft} if not defined
""",
)

cls.hide_toolbar = _add_prop_bool("/HideToolbar", f_obj)
cls.hide_menubar = _add_prop_bool("/HideMenubar", f_obj)
cls.hide_windowui = _add_prop_bool("/HideWindowUI", f_obj)
cls.fit_window = _add_prop_bool("/FitWindow", f_obj)
cls.center_window = _add_prop_bool("/CenterWindow", f_obj)
cls.display_doctitle = _add_prop_bool("/DisplayDocTitle", f_obj)

cls.non_fullscreen_pagemode = _add_prop_name(
"/NonFullScreenPageMode",
["/UseNone", "/UseOutlines", "/UseThumbs", "/UseOC"],
NameObject("/UseNone"),
)
cls.direction = _add_prop_name(
"/Direction", ["/L2R", "/R2L"], NameObject("/L2R")
)
cls.view_area = _add_prop_name("/ViewArea", [], None)
cls.view_clip = _add_prop_name("/ViewClip", [], None)
cls.print_area = _add_prop_name("/PrintArea", [], None)
cls.print_clip = _add_prop_name("/PrintClip", [], None)
cls.print_scaling = _add_prop_name("/PrintScaling", [], None)
cls.duplex = _add_prop_name(
"/Duplex", ["/Simplex", "/DuplexFlipShortEdge", "/DuplexFlipLongEdge"], None
)
cls.pick_tray_by_pdfsize = _add_prop_bool("/PickTrayByPDFSize", None)
cls.print_pagerange = _add_prop_arr("/PrintPageRange", None)
cls.num_copies = _add_prop_int("/NumCopies", None)

# still to be done /PrintPageRange and /NumCopies

return DictionaryObject.__new__(cls)

def __init__(self, obj: Optional[DictionaryObject] = None) -> None:
super().__init__(self)
if obj is not None:
self.update(obj.items())
try:
self.indirect_reference = obj.indirect_reference # type: ignore
except AttributeError:
pass
65 changes: 65 additions & 0 deletions tests/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1718,3 +1718,68 @@ def test_damaged_pdf_length_returning_none():
reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))
writer = PdfWriter()
writer.append(reader)


@pytest.mark.enable_socket()
def test_viewerpreferences():
"""
Add Tests for ViewerPreferences
https://github.com/py-pdf/pypdf/issues/140#issuecomment-1685380549
"""
url = "https://github.com/py-pdf/pypdf/files/9175966/2015._pb_decode_pg0.pdf"
name = "2015._pb_decode_pg0.pdf"
reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))
v = reader.viewer_preferences
assert v.center_window == True # noqa: E712
writer = PdfWriter(clone_from=reader)
v = writer.viewer_preferences
assert v.center_window == True # noqa: E712
v.center_window = False
assert (
writer._root_object["/ViewerPreferences"]["/CenterWindow"]
== False # noqa: E712
)
assert v.print_area == "/CropBox"
with pytest.raises(ValueError):
v.non_fullscreen_pagemode = "toto"
with pytest.raises(ValueError):
v.non_fullscreen_pagemode = "/toto"
v.non_fullscreen_pagemode = "/UseOutlines"
assert (
writer._root_object["/ViewerPreferences"]["/NonFullScreenPageMode"]
== "/UseOutlines"
)
writer = PdfWriter(clone_from=reader)
v = writer.viewer_preferences
assert v.center_window == True # noqa: E712
v.center_window = False
assert (
writer._root_object["/ViewerPreferences"]["/CenterWindow"]
== False # noqa: E712
)

writer = PdfWriter(clone_from=reader)
writer._root_object[NameObject("/ViewerPreferences")] = writer._add_object(
writer._root_object["/ViewerPreferences"]
)
v = writer.viewer_preferences
v.center_window = False
assert (
writer._root_object["/ViewerPreferences"]["/CenterWindow"]
== False # noqa: E712
)
v.num_copies = 1
assert v.num_copies == 1
assert v.print_pagerange is None
with pytest.raises(ValueError):
v.print_pagerange = "toto"
v.print_pagerange = ArrayObject()
assert len(v.print_pagerange) == 0

writer.create_viewer_preference()
assert len(writer._root_object["/ViewerPreferences"]) == 0

del reader.trailer["/Root"]["/ViewerPreferences"]
assert reader.viewer_preferences is None
writer = PdfWriter(clone_from=reader)
assert writer.viewer_preferences is None

0 comments on commit 05f2a65

Please sign in to comment.