Skip to content

Commit 6f3a50e

Browse files
committed
ENH : ease access to ViewerPreferences
closes py-pdf#2105 still doc and test to be fixed/added
1 parent 301b542 commit 6f3a50e

File tree

4 files changed

+188
-0
lines changed

4 files changed

+188
-0
lines changed

pypdf/_reader.py

+14
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
PdfObject,
104104
TextStringObject,
105105
TreeObject,
106+
ViewerPreferences,
106107
read_object,
107108
)
108109
from .types import OutlineType, PagemodeType
@@ -293,6 +294,19 @@ class PdfReader:
293294
Defaults to ``None``
294295
"""
295296

297+
@property
298+
def viewer_preferences(self) -> Optional[ViewerPreferences]:
299+
"""Returns the existing ViewerPreferences as a overloaded dictionniary."""
300+
o = cast(DictionaryObject, self.trailer["/Root"]).get(
301+
CD.VIEWER_PREFERENCES, None
302+
)
303+
if o is None:
304+
return None
305+
o = o.get_object()
306+
if not isinstance(o, ViewerPreferences):
307+
o = ViewerPreferences(o)
308+
return o
309+
296310
def __init__(
297311
self,
298312
stream: Union[StrByteType, Path],

pypdf/_writer.py

+22
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
TypFitArguments,
8383
UserAccessPermissions,
8484
)
85+
from .constants import CatalogDictionary as CD
8586
from .constants import Core as CO
8687
from .constants import (
8788
FieldDictionaryAttributes as FA,
@@ -110,6 +111,7 @@
110111
StreamObject,
111112
TextStringObject,
112113
TreeObject,
114+
ViewerPreferences,
113115
create_string_object,
114116
hex_to_rgb,
115117
)
@@ -367,6 +369,26 @@ def set_need_appearances_writer(self, state: bool = True) -> None:
367369
f"set_need_appearances_writer({state}) catch : {exc}", __name__
368370
)
369371

372+
@property
373+
def viewer_preferences(self) -> Optional[ViewerPreferences]:
374+
"""Returns the existing ViewerPreferences as a overloaded dictionniary."""
375+
o = cast(DictionaryObject, self._root_object).get(CD.VIEWER_PREFERENCES, None)
376+
if o is None:
377+
return None
378+
o = o.get_object()
379+
if not isinstance(o, ViewerPreferences):
380+
o = ViewerPreferences(o)
381+
if hasattr(o, "indirect_reference"):
382+
self._replace_object(o.indirect_reference, o)
383+
else:
384+
self._root_object[NameObject(CD.VIEWER_PREFERENCES)] = o
385+
return o
386+
387+
def create_viewer_preference(self) -> ViewerPreferences:
388+
o = ViewerPreferences()
389+
self._root_object[CD.VIEWER_PREFERENCES] = self._add_object(o)
390+
return o
391+
370392
def add_page(
371393
self,
372394
page: PageObject,

pypdf/generic/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@
6767
read_hex_string_from_stream,
6868
read_string_from_stream,
6969
)
70+
from ._viewerpref import ViewerPreferences
7071

7172

7273
def readHexStringFromStream(
@@ -443,6 +444,7 @@ def link(
443444
"RectangleObject",
444445
"Field",
445446
"Destination",
447+
"ViewerPreferences",
446448
# --- More specific stuff
447449
# Outline
448450
"OutlineItem",

pypdf/generic/_viewerpref.py

+150
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
# Copyright (c) 2023, Pubpub-ZZ
2+
#
3+
# All rights reserved.
4+
#
5+
# Redistribution and use in source and binary forms, with or without
6+
# modification, are permitted provided that the following conditions are
7+
# met:
8+
#
9+
# * Redistributions of source code must retain the above copyright notice,
10+
# this list of conditions and the following disclaimer.
11+
# * Redistributions in binary form must reproduce the above copyright notice,
12+
# this list of conditions and the following disclaimer in the documentation
13+
# and/or other materials provided with the distribution.
14+
# * The name of the author may not be used to endorse or promote products
15+
# derived from this software without specific prior written permission.
16+
#
17+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
18+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
19+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
21+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
22+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
23+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
25+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
26+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
27+
# POSSIBILITY OF SUCH DAMAGE.
28+
29+
from typing import (
30+
Any,
31+
List,
32+
Optional,
33+
)
34+
35+
from ._base import BooleanObject, NameObject, NumberObject
36+
from ._data_structures import ArrayObject, DictionaryObject
37+
38+
f_obj = BooleanObject(False)
39+
40+
41+
class ViewerPreferences(DictionaryObject):
42+
def _get_bool(self, key: str, deft: Optional[BooleanObject]) -> BooleanObject:
43+
return self.get(key, deft)
44+
45+
def _set_bool(self, key: str, v: bool) -> None:
46+
self[NameObject(key)] = BooleanObject(v is True)
47+
48+
def _get_name(self, key: str, deft: Optional[NameObject]) -> Optional[NameObject]:
49+
return self.get(key, deft)
50+
51+
def _set_name(self, key: str, lst: List[str], v: NameObject) -> None:
52+
if v[0] != "/":
53+
raise ValueError(f"{v} is not starting with '/'")
54+
if lst != [] and key not in lst:
55+
raise ValueError(f"{v} is not par of acceptable values")
56+
self[NameObject(key)] = NameObject(v)
57+
58+
def _get_arr(self, key: str) -> NumberObject:
59+
return self.get(key, ArrayObject())
60+
61+
def _set_arr(self, key: str, v: Optional[ArrayObject]) -> None:
62+
if not isinstance(v, ArrayObject):
63+
raise ValueError("ArrayObject is expected")
64+
self[NameObject(key)] = v
65+
66+
def _get_int(self, key: str, deft: Optional[NumberObject]) -> NumberObject:
67+
return self.get(key, deft)
68+
69+
def _set_int(self, key: str, v: int) -> None:
70+
self[NameObject(key)] = NumberObject(v)
71+
72+
def __new__(cls: Any, value: Any = None):
73+
def _add_prop_bool(key: str, deft: Optional[BooleanObject]) -> property:
74+
return property(
75+
lambda self: self._get_bool(key, deft),
76+
lambda self, v: self._set_bool(key, v),
77+
None,
78+
f"""
79+
Returns/Modify the status of {key}, Returns {deft} if not defined
80+
""",
81+
)
82+
83+
def _add_prop_name(
84+
key: str, lst: List[str], deft: Optional[NameObject]
85+
) -> property:
86+
return property(
87+
lambda self: self._get_name(key, deft),
88+
lambda self, v: self._set_name(key, lst, v),
89+
None,
90+
f"""
91+
Returns/Modify the status of {key}, Returns {deft} if not defined.
92+
Acceptable values: {lst}
93+
""",
94+
)
95+
96+
def _add_prop_arr(key: str, deft: Optional[ArrayObject]) -> property:
97+
return property(
98+
lambda self: self._get_arr(key),
99+
lambda self, v: self._set_arr(key, v),
100+
None,
101+
f"""
102+
Returns/Modify the status of {key}, Returns {deft} if not defined
103+
""",
104+
)
105+
106+
def _add_prop_int(key: str, deft: Optional[int]) -> property:
107+
return property(
108+
lambda self: self._get_int(key, deft),
109+
lambda self, v: self._set_int(key, v),
110+
None,
111+
f"""
112+
Returns/Modify the status of {key}, Returns {deft} if not defined
113+
""",
114+
)
115+
116+
cls.hide_toolbar = _add_prop_bool("/HideToolbar", f_obj)
117+
cls.hide_menubar = _add_prop_bool("/HideMenubar", f_obj)
118+
cls.hide_windowui = _add_prop_bool("/HideWindowUI", f_obj)
119+
cls.fit_window = _add_prop_bool("/FitWindow", f_obj)
120+
cls.center_window = _add_prop_bool("/CenterWindow", f_obj)
121+
cls.display_doctitle = _add_prop_bool("/DisplayDocTitle", f_obj)
122+
123+
cls.non_fullscreen_pagemode = _add_prop_name(
124+
"/NonFullScreenPageMode",
125+
["/UseNone", "/UseOutlines", "/UseThumbs", "/UseOC"],
126+
NameObject("/UseNone"),
127+
)
128+
cls.direction = _add_prop_name(
129+
"/Direction", ["/L2R", "/R2L"], NameObject("/L2R")
130+
)
131+
cls.view_area = _add_prop_name("/ViewArea", [], None)
132+
cls.view_clip = _add_prop_name("/ViewClip", [], None)
133+
cls.print_area = _add_prop_name("/PrintArea", [], None)
134+
cls.print_clip = _add_prop_name("/PrintClip", [], None)
135+
cls.print_scaling = _add_prop_name("/PrintScaling", [], None)
136+
cls.duplex = _add_prop_name(
137+
"/Duplex", ["/Simplex", "/DuplexFlipShortEdge", "/DuplexFlipLongEdge"], None
138+
)
139+
cls.pick_tray_by_pdfsize = _add_prop_bool("/PickTrayByPDFSize", None)
140+
cls.print_pagerange = _add_prop_arr("/PrintPageRange", None)
141+
cls.num_copies = _add_prop_int("/NumCopies", None)
142+
143+
# still to be done /PrintPageRange and /NumCopies
144+
145+
return DictionaryObject.__new__(cls)
146+
147+
def __init__(self, obj: Optional[DictionaryObject] = None) -> None:
148+
super().__init__(self)
149+
if obj is not None:
150+
self.update(obj.items())

0 commit comments

Comments
 (0)