def get_text_commands( x1, y1, x2, y2, text, font_size, wrap_text, align, baseline, line_spacing, ): """Return the graphics stream commands necessary to render a free text annotation, given the various parameters. Text is optionally wrapped, then arranged according to align (horizontal alignment), and baseline (vertical alignment). :param number x1: bounding box lower left x :param number y1: bounding box lower left y :param number x2: bounding box upper right x :param number y2: bounding box upper right y :param str text: text to add to annotation :param number font_size: font size :param bool wrap_text: whether to wrap the text :param str align: 'left'|'center'|'right' :param str baseline: 'top'|'middle'|'bottom' :param number line_spacing: multiplier to determine line spacing """ font = get_true_type_font( path=HELVETICA_PATH, font_name=DEFAULT_BASE_FONT, font_size=font_size, ) lines = get_wrapped_lines( text=text, measure=font.measure_text, max_length=x2 - x1, ) if wrap_text else [text] # Line breaking cares about the whitespace in the string, but for the # purposes of laying out the broken lines, we want to measure the lines # without trailing/leading whitespace. lines = [line.strip() for line in lines] y_coords = _get_vertical_coordinates( lines, y1, y2, font_size, line_spacing, baseline, ) xs = _get_horizontal_coordinates(lines, x1, x2, font.measure_text, align) commands = [] for line, x, y in zip(lines, xs, y_coords): commands.extend([ TextMatrix(translate(x, y)), Text(line), ]) return commands
def get_ctm(x1, y1, x2, y2): """Get the scaled and translated CTM for an image to be placed in the bounding box defined by [x1, y1, x2, y2]. """ return matrix_multiply( translate(x1, y1), scale(x2 - x1, y2 - y1), )
def test_multiply_is_associative(self): R = rotate(45) S = scale(3, 2) T = translate(5, -1) # T*(S*R) M1 = matrix_multiply(T, matrix_multiply(S, R)) # (T*S)*R M2 = matrix_multiply(matrix_multiply(T, S), R) assert M1 == M2 assert M1 == matrix_multiply(T, S, R)
def _get_transform(bounding_box, rotation, _scale): """Get the transformation required to go from the user's desired coordinate space to PDF user space, taking into account rotation, scaling, translation (for things like weird media boxes). """ # Unrotated width and height, in pts W = bounding_box[2] - bounding_box[0] H = bounding_box[3] - bounding_box[1] scale_matrix = scale(*_scale) x_translate = 0 + min(bounding_box[0], bounding_box[2]) y_translate = 0 + min(bounding_box[1], bounding_box[3]) mb_translate = translate(x_translate, y_translate) # Because of how rotation works the point isn't rotated around an axis, # but the axis itself shifts. So we have to represent the rotation as # rotation + translation. rotation_matrix = rotate(rotation) translate_matrix = identity() if rotation == 90: translate_matrix = translate(W, 0) elif rotation == 180: translate_matrix = translate(W, H) elif rotation == 270: translate_matrix = translate(0, H) # Order matters here - the transformation matrices are applied in # reverse order. So first we scale to get the points in PDF user space, # since all other operations are in that space. Then we rotate and # scale to capture page rotation, then finally we translate to account # for offset media boxes. transform = matrix_multiply( mb_translate, translate_matrix, rotation_matrix, scale_matrix, ) return transform
def test_as_pdf_object(self): x1, y1, x2, y2 = 10, 20, 100, 200 image = Image( location=Location(x1=x1, y1=y1, x2=x2, y2=y2, page=0), appearance=Appearance(stroke_width=0, image=PNG_FILES[0]), ) obj = image.as_pdf_object(identity(), page=None) # Appearance stream should place the Image correctly assert obj.AP.N.stream == ( 'q 0 0 0 RG 0 w 10 20 90 180 re 90 0 0 180 10 20 cm /Image Do Q') assert obj.Rect == [x1, y1, x2, y2] assert obj.AP.N.BBox == [x1, y1, x2, y2] assert obj.AP.N.Matrix == translate(-x1, -y1)
def test_pdf_object(self): x1, y1, x2, y2 = 10, 20, 100, 200 annotation = FreeText( Location(x1=x1, y1=y1, x2=x2, y2=y2, page=0), Appearance( fill=[0.4, 0, 0], stroke_width=1, font_size=5, content='Hi', ), ) obj = annotation.as_pdf_object(identity(), page=None) assert obj.AP.N.stream == ( 'q BT 0.4 0 0 rg /{} 5 Tf ' '1 0 0 1 11 109 Tm (Hi) Tj ET Q').format(PDF_ANNOTATOR_FONT) assert obj.DA == '0.4 0 0 rg /{} 5 Tf'.format(PDF_ANNOTATOR_FONT) assert obj.Rect == [x1, y1, x2, y2] assert obj.AP.N.BBox == [x1, y1, x2, y2] assert obj.AP.N.Matrix == translate(-x1, -y1)
def _make_appearance_stream_dict(self, bounding_box, transform): resources = self._make_ap_resources() # Either use user-specified content stream or generate content stream # based on annotation type. stream = self._appearance.appearance_stream if stream is None: stream = self.make_appearance_stream() # Transform the appearance stream into PDF space and turn it into a str appearance_stream = stream.transform(transform).resolve() normal_appearance = PdfDict( stream=appearance_stream, BBox=bounding_box, Resources=resources, Matrix=translate(-bounding_box[0], -bounding_box[1]), Type=PdfName('XObject'), Subtype=PdfName('Form'), FormType=1, ) return PdfDict(N=normal_appearance)
def test_invert_translate(self): assert translate(-3, -5) == matrix_inverse(translate(3, 5)) assert translate(-2, 1) == matrix_inverse(translate(2, -1))
def test_translates_add(self): T1 = translate(3, 5) T2 = translate(2, -1) assert translate(5, 4) == matrix_multiply(T1, T2) assert translate(5, 4) == matrix_multiply(T2, T1)
def test_weird_bounding_box(self): self._assert_transform(translate(0, -30), bounding_box=[0, -30, 20, 0])