Home > OS >  Use PyMuPDF to bold parts of text
Use PyMuPDF to bold parts of text

Time:05-26

I am trying to use PyMuPDF to bold portions of each word in a PDF file.

So, for example, a file with the string "There are many pies" will result in "There are many pies"

I have seen that you can use Page.get_textpage().extractWORDS() to sort of extract a tuple of the various words. However, I'm not sure exactly how to bold portions of them.

I had thought maybe you could erase them and then re-write them, but I'm not sure if PyMuPDF can erase words.

CodePudding user response:

It looks like PyMuPDF Original text Highlighted text

I bet the effect would be complete by creating new text on top of the original (maybe still redacted) text, but I've run out of time, for now, to try that out.

import fitz

from fitz import Document, Page
from fitz import Matrix, Point, Rect


Normal_style = dict(fontname="helv", fontsize=24)
Bold_style = dict(fontname="hebo", fontsize=24)

RawDictChar = dict  # See "Character Dictionary for extractRAWDICT()" in PyMuPDF docs
CharSegment = list[RawDictChar]


def main():
    doc: Document = fitz.open()
    page: Page = doc.new_page()

    page.insert_text(Point(50, 72), "A number of words and things on line 1", **Normal_style)
    page.insert_text(Point(50, 144), "A number of words on line 2", **Normal_style)
    page.insert_text(Point(50, 216), "Line 3", **Normal_style)

    page_to_image(page, "page-orig.png")

    char_segments = get_char_segments(page)

    apply_segment_redactions(page, char_segments)

    page_to_image(page, "page-edit.png")


def get_char_segments(page: Page, num_chars: int = 3) -> list[CharSegment]:
    """
    Breaks a page down in groups ("segments") of individual characters, and returns a list of these "character segments".

    Each character segment is at most `num_chars` long and will be the first number of characters of a word (delimited by a space).
    """
    char_segments: list[CharSegment] = []

    rawdict = page.get_text("rawdict")
    for block in rawdict["blocks"]:
        if block["type"] == 1:
            continue  # skip "image" block

        for line in block["lines"]:
            for span in line["spans"]:
                chars = span["chars"]
                word_chars = []
                for char in chars:
                    # Break on "space"
                    if char["c"] == " ":
                        char_segments.append(word_chars[:num_chars])
                        word_chars = []
                        continue

                    word_chars.append(char)

                # Get any end-of-line chars
                if word_chars:
                    char_segments.append(word_chars[:num_chars])

    return char_segments


def apply_segment_redactions(page: Page, char_segments: list[CharSegment]):
    """Turns each character segment into a redaction annotation, applying the same characters but now in a boldened font."""
    M_shift_down = Matrix(1, 1).pretranslate(0, 2.5)  # try to compensate for redactions being vertically centered

    for char_segment in char_segments:
        first_cs = char_segment[0]

        # Build up replacement/redaction text
        highlight_txt = first_cs["c"]
        # Build up "super rect" of redaction area through rectangle unions of each subsequent char in segment
        highlight_rect: Rect = Rect(*first_cs["bbox"])

        for cs in char_segment[1:]:
            highlight_rect = highlight_rect | Rect(*cs["bbox"])
            highlight_txt  = cs["c"]

        highlight_rect.transform(M_shift_down)

        page.add_redact_annot(highlight_rect, text=highlight_txt, fill=False, **Bold_style)

    page.apply_redactions(images=fitz.PDF_REDACT_IMAGE_NONE)


def page_to_image(page: Page, fname):
    """Helper to visualize the original and redacted/highlighted."""
    Zoom_x = 2.0  # horizontal zoom
    Zoom_y = 2.0  # vertical zoom
    Z_matrix = fitz.Matrix(Zoom_x, Zoom_y)  # zoom factor 2 in each dimension

    pix = page.get_pixmap(matrix=Z_matrix)  # use 'mat' instead of the identity matrix
    pix.save(fname)  # store image as a PNG


if __name__ == "__main__":
    main()
  • Related