Search code examples
pythonpython-3.xpdfborb

How to rotate only Table, using borb (PDF library) python?


I am using borb (PDF library in python). I need to rotate Only the table. (Also the text inside the table should be rotated as per the table rotation)

  • Say for example, if I am rotating the Table to left (i.e)(90° left_side), text inside the table should also be rotated. (similarly for right rotation)
  • In short, only the Content should be rotated.

Here is my code.py

from decimal import Decimal
from pathlib import Path

from borb.pdf.canvas.layout.image.image import Image
from borb.pdf.canvas.layout.layout_element import Alignment
from borb.pdf.canvas.layout.page_layout.multi_column_layout import SingleColumnLayout
from borb.pdf.canvas.layout.page_layout.page_layout import PageLayout
from borb.pdf.canvas.color.color import HexColor
from borb.pdf.canvas.layout.table.fixed_column_width_table import FixedColumnWidthTable
from borb.pdf.canvas.layout.table.flexible_column_width_table import FlexibleColumnWidthTable
from borb.pdf.canvas.layout.table.table import TableCell
from borb.pdf.canvas.layout.text.chunk_of_text import ChunkOfText
from borb.pdf.canvas.layout.text.paragraph import Paragraph
from borb.pdf.document.document import Document
from borb.pdf.page.page import Page
from borb.pdf.pdf import PDF




def main():
    # define theme color

    # create new Document
    doc: Document = Document()

    # create new Page
    page: Page = Page()
    doc.add_page(page)

    # set PageLayout
    layout: PageLayout = SingleColumnLayout(page, horizontal_margin=Decimal(25), vertical_margin=Decimal(25))


    layout.add(Paragraph("Welcome"))


    layout.add(
        FixedColumnWidthTable( number_of_columns=1, number_of_rows=2)
            .add(Paragraph("Testing_Line1"))
            .add(Paragraph("Testing_Line2")))




    with open("output.pdf", "wb") as out_file_handle:
        PDF.dumps(out_file_handle, doc)


if __name__ == "__main__":
    main()

Solution

  • disclaimer: I am the author of borb

    I have the following code (which attempts to generically rotate any LayoutElement)

    import math
    import typing
    from decimal import Decimal
    
    from borb.pdf.canvas.geometry.rectangle import Rectangle
    from borb.pdf.canvas.layout.layout_element import LayoutElement
    
    
    def rotate_point(angle_in_degrees: Decimal,
                     point: typing.Tuple[Decimal, Decimal]) -> typing.Tuple[Decimal, Decimal]:
        # convert angle to radians
        angle_in_radians: Decimal = Decimal(math.radians(angle_in_degrees))
    
        # perform rotation
        x: Decimal = point[0]
        y: Decimal = point[1]
        x_prime: Decimal = x * Decimal(math.cos(angle_in_radians)) - y * Decimal(math.sin(angle_in_radians))
        y_prime: Decimal = x * Decimal(math.sin(angle_in_radians)) + y * Decimal(math.cos(angle_in_radians))
    
        # return
        return x_prime, y_prime
    
    
    def dimensions_of_rotated_rectangle(r: Rectangle,
                                        angle_in_degrees: Decimal) -> Rectangle:
        ZERO: Decimal = Decimal(0)
        W: Decimal = r.get_width()
        H: Decimal = r.get_height()
        p0: typing.Tuple[Decimal, Decimal] = rotate_point(angle_in_degrees=angle_in_degrees, point=(ZERO, ZERO))
        p1: typing.Tuple[Decimal, Decimal] = rotate_point(angle_in_degrees=angle_in_degrees, point=(ZERO, H))
        p2: typing.Tuple[Decimal, Decimal] = rotate_point(angle_in_degrees=angle_in_degrees, point=(W, H))
        p3: typing.Tuple[Decimal, Decimal] = rotate_point(angle_in_degrees=angle_in_degrees, point=(W, ZERO))
    
        # calculate width and height
        w_prime: Decimal = max([p0[0], p1[0], p2[0], p3[0]]) - min([p0[0], p1[0], p2[0], p3[0]])
        h_prime: Decimal = max([p0[1], p1[1], p2[1], p3[1]]) - min([p0[1], p1[1], p2[1], p3[1]])
    
        # return
        return Rectangle(ZERO, ZERO, w_prime, h_prime)
    
    def delta_of_rotated_rectangle(r: Rectangle,
                                   angle_in_degrees: Decimal) -> typing.Tuple[Decimal, Decimal]:
        ZERO: Decimal = Decimal(0)
        W: Decimal = r.get_width()
        H: Decimal = r.get_height()
        p0: typing.Tuple[Decimal, Decimal] = rotate_point(angle_in_degrees=angle_in_degrees, point=(ZERO, ZERO))
        p1: typing.Tuple[Decimal, Decimal] = rotate_point(angle_in_degrees=angle_in_degrees, point=(ZERO, H))
        p2: typing.Tuple[Decimal, Decimal] = rotate_point(angle_in_degrees=angle_in_degrees, point=(W, H))
        p3: typing.Tuple[Decimal, Decimal] = rotate_point(angle_in_degrees=angle_in_degrees, point=(W, ZERO))
    
        return (-min([p0[0], p1[0], p2[0], p3[0]]),
                -min([p0[1], p1[1], p2[1], p3[1]]))
    
    def largest_inscribed_rectangle(angle_in_degrees: Decimal,
                                    r: Rectangle) -> Rectangle:
        max_area: typing.Optional[Decimal] = None
        max_w: typing.Optional[Decimal] = None
        max_h: typing.Optional[Decimal] = None
        for w in range(int(r.get_width() // 2), int(r.get_width()) + 1):
            for h in range(int(r.get_height() // 2), int(r.get_height()) + 1):
                r2: Rectangle = dimensions_of_rotated_rectangle(Rectangle(Decimal(0),
                                                                          Decimal(0),
                                                                          Decimal(w),
                                                                          Decimal(h)),
                                                                angle_in_degrees=angle_in_degrees)
                if r2.get_width() > r.get_width():
                    continue
                if r2.get_height() > r.get_height():
                    continue
                area: Decimal = r2.get_width() * r2.get_height()
                if max_area is None or area > max_area:
                    max_area = area
                    max_w = Decimal(w)
                    max_h = Decimal(h)
    
        assert max_w is not None
        assert max_h is not None
    
        # return
        return Rectangle(Decimal(0),
                         Decimal(0),
                         max_w,
                         max_h)
    
    
    class RotatedLayoutElement(LayoutElement):
    
        def __init__(self, angle_in_degrees: Decimal, layout_element: LayoutElement):
            super().__init__()
            self._angle_in_degrees: Decimal = angle_in_degrees
            self._layout_element: LayoutElement = layout_element
            self._prev_content_box: typing.Optional[Rectangle] = None
        #
        # PRIVATE
        #
    
        def _get_content_box(self, available_space: Rectangle) -> Rectangle:
            r0: Rectangle = largest_inscribed_rectangle(r=available_space,
                                                        angle_in_degrees=self._angle_in_degrees)
            self._prev_inner_content_box = self._layout_element.get_layout_box(r0)
            r2: Rectangle = dimensions_of_rotated_rectangle(self._prev_inner_content_box, self._angle_in_degrees)
            self._prev_content_box = Rectangle(available_space.get_x(),
                                               available_space.get_y() + available_space.get_height() - r2.get_height(),
                                               r2.get_width(),
                                               r2.get_height())
            return self._prev_content_box
    
        def _paint_content_box(self, page: "Page", content_box: Rectangle) -> None:
    
            # we are going to do unholy things to the graphics state
            # best to store it before the madness begins
            page.append_to_content_stream(" q ")
    
            # rotate
            page.append_to_content_stream(f"{round(math.cos(math.radians(self._angle_in_degrees)), 2)} "
                                          f"{round(math.sin(math.radians(self._angle_in_degrees)), 2)} "
                                          f"{round(-math.sin(math.radians(self._angle_in_degrees)), 2)} "
                                          f"{round(math.cos(math.radians(self._angle_in_degrees)), 2)} "
                                          f"0 0 cm ")
    
            # translate to counteract translation by rotation
            # ensuring the bounding rectangle fits inside the target rectangle
            tx, ty = rotate_point(angle_in_degrees=-self._angle_in_degrees,
                                  point=delta_of_rotated_rectangle(self._prev_inner_content_box,
                                                                   angle_in_degrees=self._angle_in_degrees))
            page.append_to_content_stream(f"1 0 0 1 {round(tx, 2)} {round(ty, 2)} cm ")
    
            # translate to point
            tx, ty = rotate_point(point=(content_box.get_x(),
                                         content_box.get_y()), angle_in_degrees=-self._angle_in_degrees)
            page.append_to_content_stream(f"1 0 0 1 {round(tx, 2)} {round(ty, 2)} cm ")
    
            # paint
            self._layout_element.paint(page, Rectangle(Decimal(0),
                                                       Decimal(0),
                                                       self._prev_inner_content_box.get_width(),
                                                       self._prev_inner_content_box.get_height()))
    
            # restore graphics state
            page.append_to_content_stream(" Q ")
    

    Copy/paste that, store as rotated_layout_element.py

    Now you can use that to do the following:

    from decimal import Decimal
    
    from borb.pdf import Document
    from borb.pdf import PDF
    from borb.pdf import Page
    from borb.pdf import PageLayout
    from borb.pdf import Paragraph
    from borb.pdf import SingleColumnLayout
    from borb.pdf import TableUtil
    
    from rotated_layout_element import RotatedLayoutElement
    
    
    def main():
    
        d: Document = Document()
    
        p: Page = Page()
        d.add_page(p)
    
        l: PageLayout = SingleColumnLayout(p)
    
        l.add(Paragraph("27 degrees"))
        l.add(
                RotatedLayoutElement(
                    layout_element=TableUtil.from_2d_array([["Lorem", "Ipsum", "Dolor", "Sit", "Amet"],
                                                            [1,2,3,4,5],
                                                            [4,5,6,7,8]]),
                    angle_in_degrees=Decimal(27),
                )
            )
    
    
        with open("output.pdf", "wb") as fh:
            PDF.dumps(fh, d)
    
    
    if __name__ == "__main__":
        main()
    
    

    Which yields the following PDF:

    enter image description here