Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ENH: ease Permissions readability/writability #2397

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions pypdf/_reader.py
Original file line number Diff line number Diff line change
Expand Up @@ -1808,8 +1808,28 @@ def decrypt(self, password: Union[str, bytes]) -> PasswordType:
# TODO: raise Exception for wrong password
return self._encryption.verify(password)

def decode_permissions(self, permissions_code: int) -> Dict[str, bool]:
# Takes the permissions as an integer, returns the allowed access
def decode_permissions(
self, permissions_code: Optional[int] = None
) -> Dict[str, bool]:
"""
Decodes the permissions as a dictionary

Args:
permissions_code: integer providing bits from an integer
if None is provided, the value is retrieved from the default pdf fields
Individual permissions using PKCS#7 shall be extracted manually

Returns:
permissions as human readable structure in a dict
Can be used to feed permissions_flag within PdfWriter.encrypt
"""
if permissions_code is None:
permissions_code = cast(
int,
cast(DictionaryObject, self.trailer[TK.ENCRYPT]).get("/P", -1)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not use self._encryption.P here?

if TK.ENCRYPT in self.trailer
else -1,
)
permissions = {}
permissions["print"] = permissions_code & (1 << 3 - 1) != 0 # bit 3
permissions["modify"] = permissions_code & (1 << 4 - 1) != 0 # bit 4
Expand Down
28 changes: 26 additions & 2 deletions pypdf/_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@
)

OPTIONAL_READ_WRITE_FIELD = FieldFlag(0)
ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions((2**31 - 1) - 3)
ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions((2**32 - 1) - 3)


class ObjectDeletionFlag(enum.IntFlag):
Expand Down Expand Up @@ -1065,7 +1065,9 @@ def encrypt(
user_password: str,
owner_password: Optional[str] = None,
use_128bit: bool = True,
permissions_flag: UserAccessPermissions = ALL_DOCUMENT_PERMISSIONS,
permissions_flag: Union[
UserAccessPermissions, Dict[str, bool]
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we really complicate this API further? We cannot we just provide a convenience method on UserAccessPermissions instead?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd also prefer not to add the string versions here. I regret adding them in the first place 🙈

] = ALL_DOCUMENT_PERMISSIONS,
*,
algorithm: Optional[str] = None,
) -> None:
Expand All @@ -1088,13 +1090,35 @@ def encrypt(
Bit position 3 is for printing, 4 is for modifying content,
5 and 6 control annotations, 9 for form fields,
10 for extraction of text and graphics.
permissions can be provided as an integer or as a dictionary
as provided by `PdfReader.decode_permissions()`
algorithm: encrypt algorithm. Values maybe one of "RC4-40", "RC4-128",
"AES-128", "AES-256-R5", "AES-256". If it's valid,
`use_128bit` will be ignored.
"""
if owner_password is None:
owner_password = user_password

if isinstance(permissions_flag, dict):
permissions = permissions_flag
_flag = cast(int, ALL_DOCUMENT_PERMISSIONS)
if not permissions.get("print", False):
_flag -= 1 << 2
if not permissions.get("modify", False):
_flag -= 1 << 3
if not permissions.get("copy", False):
_flag -= 1 << 4
if not permissions.get("annotations", False):
_flag -= 1 << 5
if not permissions.get("forms", False):
_flag -= 1 << 8
if not permissions.get("accessability", False):
_flag -= 1 << 9
if not permissions.get("assemble", False):
_flag -= 1 << 10
if not permissions.get("print_high_quality", False):
_flag -= 1 << 11
permissions_flag = cast(UserAccessPermissions, _flag)
if algorithm is not None:
try:
alg = getattr(EncryptAlgorithm, algorithm.replace("-", "_"))
Expand Down
16 changes: 16 additions & 0 deletions tests/test_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -1985,3 +1985,19 @@ def create_number_pdf(n) -> BytesIO:
for n, page in enumerate(reader.pages):
text = page.extract_text()
assert text == str(n)


def test_write_permissions():
r = PdfReader(RESOURCE_ROOT / "crazyones.pdf")
p = r.decode_permissions()
for k in p:
assert p[k]
np = dict(p)
np[k] = False
w = PdfWriter(r)
w.encrypt("", permissions_flag=np)
b = BytesIO()
w.write(b)
rr = PdfReader(b)
# print(rr.trailer["/Encrypt"]["/P"],rr.decode_permissions())
assert not rr.decode_permissions()[k]
Loading