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

Fix rotate #501

Open
wants to merge 8 commits into
base: master
Choose a base branch
from
Open
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
12 changes: 11 additions & 1 deletion pyhanko/pdf_utils/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from enum import Enum
from typing import Optional

from pyhanko.pdf_utils import generic
from .generic import (
DictionaryObject,
NameObject,
Expand Down Expand Up @@ -174,15 +175,22 @@ class PdfContent:
It can also be set after the fact by calling :meth:`set_writer`.
"""

matrix = None
"""
Transformation Matrix that would rotate the content
"""

def __init__(
self,
resources: Optional[PdfResources] = None,
box: Optional[BoxConstraints] = None,
writer: Optional[BasePdfFileWriter] = None,
matrix: Optional[generic.DictionaryObject] = None,
):
self._resources: PdfResources = resources or PdfResources()
self.box: BoxConstraints = box or BoxConstraints()
self.writer = writer
self.matrix = matrix

@property
def _ensure_writer(self) -> BasePdfFileWriter:
Expand Down Expand Up @@ -254,6 +262,7 @@ def as_form_xobject(self) -> StreamObject:
box_width=self.box.width,
box_height=self.box.height,
resources=self._resources.as_pdf_object(),
matrix=self.matrix,
)

def set_writer(self, writer):
Expand Down Expand Up @@ -304,8 +313,9 @@ def __init__(
data: bytes,
resources: Optional[PdfResources] = None,
box: Optional[BoxConstraints] = None,
matrix: Optional[generic.DictionaryObject] = None,
):
super().__init__(resources, box)
super().__init__(resources, box, matrix=matrix)
self.data = data

def render(self) -> bytes:
Expand Down
38 changes: 22 additions & 16 deletions pyhanko/pdf_utils/writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ def init_xobject_dictionary(
box_width,
box_height,
resources: Optional[generic.DictionaryObject] = None,
matrix: Optional[generic.DictionaryObject] = None,
) -> generic.StreamObject:
"""
Helper function to initialise form XObject dictionaries.
Expand All @@ -78,21 +79,26 @@ def init_xobject_dictionary(
The height of the XObject's bounding box.
:param resources:
A resource dictionary to include with the form object.
:param matrix:
A resource dictionary to include matrix object within rotated page.
:return:
A :class:`~.generic.StreamObject` representation of the form XObject.
"""
resources = resources or generic.DictionaryObject()
dict_data = {
pdf_name('/BBox'): generic.ArrayObject(
list(map(generic.FloatObject, (0.0, box_height, box_width, 0.0)))
),
pdf_name('/Resources'): resources,
pdf_name('/Type'): pdf_name('/XObject'),
pdf_name('/Subtype'): pdf_name('/Form'),
}
if matrix is not None:
dict_data[pdf_name('/Matrix')] = generic.ArrayObject(
list(map(generic.FloatObject, matrix))
)
return generic.StreamObject(
{
pdf_name('/BBox'): generic.ArrayObject(
list(
map(generic.FloatObject, (0.0, box_height, box_width, 0.0))
)
),
pdf_name('/Resources'): resources,
pdf_name('/Type'): pdf_name('/XObject'),
pdf_name('/Subtype'): pdf_name('/Form'),
},
dict_data,
stream_data=command_stream,
)

Expand Down Expand Up @@ -934,7 +940,7 @@ def add_stream_to_page(
# mark the page to be updated as well
self.mark_update(page_obj_ref)
else:
raise PdfError('Unexpected type for page /Contents')
raise PdfError("Unexpected type for page /Contents")
elif isinstance(contents_ref, generic.ArrayObject):
# make /Contents an indirect array, and append our stream
contents = contents_ref
Expand All @@ -945,7 +951,7 @@ def add_stream_to_page(
page_obj[pdf_name('/Contents')] = self.add_object(contents)
self.mark_update(page_obj_ref)
else:
raise PdfError('Unexpected type for page /Contents')
raise PdfError("Unexpected type for page /Contents")

if resources is None:
return
Expand Down Expand Up @@ -1061,7 +1067,7 @@ def __init__(self, stream_xrefs=True, init_page_tree=True, info=None):
# root object
root = generic.DictionaryObject(
{
pdf_name("/Type"): pdf_name("/Catalog"),
pdf_name('/Type'): pdf_name('/Catalog'),
}
)

Expand All @@ -1075,9 +1081,9 @@ def __init__(self, stream_xrefs=True, init_page_tree=True, info=None):
if init_page_tree:
pages = generic.DictionaryObject(
{
pdf_name("/Type"): pdf_name("/Pages"),
pdf_name("/Count"): generic.NumberObject(0),
pdf_name("/Kids"): generic.ArrayObject(),
pdf_name('/Type'): pdf_name('/Pages'),
pdf_name('/Count'): generic.NumberObject(0),
pdf_name('/Kids'): generic.ArrayObject(),
}
)

Expand Down
56 changes: 49 additions & 7 deletions pyhanko/sign/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,8 +328,8 @@ def _as_tuple(with_val):
return tuple(1 if val == with_val else 0 for val in ku_str)

return SigCertKeyUsage(
must_have=KeyUsage(_as_tuple('1')),
forbidden=KeyUsage(_as_tuple('0')),
must_have=KeyUsage(_as_tuple("1")),
forbidden=KeyUsage(_as_tuple("0")),
)

@classmethod
Expand Down Expand Up @@ -1483,20 +1483,20 @@ def prepare_sig_field(
field_name, value, sig_field_ref = next(candidates)
if value is not None:
raise SigningError(
'Signature field with name %s appears to be filled already.'
"Signature field with name %s appears to be filled already."
% sig_field_name
)
except StopIteration:
if existing_fields_only:
raise SigningError(
'No empty signature field with name %s found.'
"No empty signature field with name %s found."
% sig_field_name
)
form_created = False
except KeyError:
# we have to create the form
if existing_fields_only:
raise SigningError('This file does not contain a form.')
raise SigningError("This file does not contain a form.")
# no AcroForm present, so create one
form = generic.DictionaryObject()
root[pdf_name('/AcroForm')] = update_writer.add_object(form)
Expand Down Expand Up @@ -1736,6 +1736,16 @@ def append_signature_field(

if sig_field_spec.box is not None:
llx, lly, urx, ury = sig_field_spec.box
matrix = None
pagetree_obj = page_ref.get_object()
obj = pagetree_obj.get('/Rotate', 0)
rotation = obj if isinstance(obj, int) else obj.get_object()
if rotation == 90:
matrix = [0.0, 1.0, -1.0, 0.0, 0.0, 0.0]
elif rotation == 180:
matrix = [-1.0, 0.0, 0.0, -1.0, 0.0, 0.0]
elif rotation == 270:
matrix = [0.0, -1.0, 1.0, 0.0, 0.0, 0.0]
w = abs(urx - llx)
h = abs(ury - lly)
if w and h:
Expand All @@ -1753,10 +1763,11 @@ def append_signature_field(
ap_stream = RawContent(
b' '.join(appearance_cmds),
box=BoxConstraints(width=w, height=h),
matrix=matrix,
).as_form_xobject()
else:
ap_stream = RawContent(
b'', box=BoxConstraints(width=w, height=h)
b'', box=BoxConstraints(width=w, height=h), matrix=matrix
).as_form_xobject()
ap_dict[pdf_name('/N')] = pdf_out.add_object(ap_stream)

Expand Down Expand Up @@ -1842,7 +1853,38 @@ def __init__(
annot_flags |= 0b10000

annot_dict['/F'] = generic.NumberObject(annot_flags)
annot_dict['/Rect'] = generic.ArrayObject(rect)

pagetree_obj = include_on_page.get_object()
obj = pagetree_obj.get('/Rotate', 0)
while True:
try:
media_box = pagetree_obj['/MediaBox']
break
except KeyError:
try:
pagetree_obj = pagetree_obj['/Parent']
except KeyError: # pragma: nocover
raise PdfReadError(
f'Page does not have a /MediaBox'
)
page_width = generic.FloatObject(media_box[2] - media_box[0])
page_height = generic.FloatObject(media_box[3] - media_box[1])
x1, y1, x2, y2 = rect
rotate = obj if isinstance(obj, int) else obj.get_object()
if rotate == 90:
rect = [page_width - y2, x1, page_width - y1, x2]
elif rotate == 180:
rect = [
page_width - x2,
page_height - y2,
page_width - x1,
page_height - y1,
]
elif rotate == 270:
rect = [y1, page_height - x2, y2, page_height - x1]
annot_dict['/Rect'] = generic.ArrayObject(
list(map(generic.FloatObject, rect))
)

self.page_ref = include_on_page
if include_on_page is not None:
Expand Down
28 changes: 22 additions & 6 deletions pyhanko/sign/signers/cms_embedder.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class SigAppearanceSetup:
stamp style.
"""

def apply(self, sig_annot, writer):
def apply(self, sig_annot, writer, rotate):
"""
Apply the settings to an annotation.

Expand All @@ -261,16 +261,26 @@ def apply(self, sig_annot, writer):
# the field is probably a visible one, so we change its appearance
# stream to show some data about the signature
stamp = self._appearance_stamp(
writer, BoxConstraints(width=w, height=h)
writer, BoxConstraints(width=w, height=h), rotate
)
sig_annot['/AP'] = stamp.as_appearances().as_pdf_object()
normalappearence = stamp.as_appearances().as_pdf_object()
# Remove this when appearence stream in sig_annot['/AP'] is no longer replaced with the one from 'normalappearence'
if (
'/AP' in sig_annot
and '/N' in sig_annot['/AP']
and '/Matrix' in sig_annot['/AP']['/N']
):
normalappearence['/N']['/Matrix'] = sig_annot['/AP']['/N'][
'/Matrix'
]
sig_annot['/AP'] = normalappearence
try:
# if there was an entry like this, it's meaningless now
del sig_annot[pdf_name('/AS')]
except KeyError:
pass

def _appearance_stamp(self, writer, box):
def _appearance_stamp(self, writer, box, rotate):
style = self.style

name = self.name
Expand All @@ -283,7 +293,7 @@ def _appearance_stamp(self, writer, box):
if isinstance(style, TextStampStyle):
text_params['ts'] = timestamp.strftime(style.timestamp_format)

return style.create_stamp(writer, box, text_params)
return style.create_stamp(writer, box, text_params, rotate)


@dataclass(frozen=True)
Expand Down Expand Up @@ -465,7 +475,13 @@ def write_cms(
appearance_setup = sig_obj_setup.appearance_setup
if appearance_setup is not None:
sig_annot = get_sig_field_annot(sig_field)
appearance_setup.apply(sig_annot, writer)
page_ref = writer.find_page_for_modification(
new_field_spec.on_page
)[0]
ref = page_ref.get_object()
obj = ref.get('/Rotate', 0)
rotate = obj if isinstance(obj, int) else obj.get_object()
appearance_setup.apply(sig_annot, writer, rotate)

sig_obj = sig_obj_setup.sig_placeholder
sig_obj_ref = writer.add_object(sig_obj)
Expand Down
Loading