Repository: robocorp/rpaframework

Issue:
RPA. PDF Find Text
Hi there, have you ever considered to use keyword Find Text for line written PDF (without anchors?). That would be used for "basic" pdf format.
Thanks


Discussion:
Hi, can you provide a more detailed example for that to understand it better? If you're referring to use `Find Text` to just obtain the text you're looking for only, maybe [Get Text From Pdf](https://robocorp.com/docs/libraries/rpa-framework/rpa-pdf/keywords#get-text-from-pdf) in combination with [Get Lines Matching Regexp](https://robocorp.com/docs/libraries/built-in/string/keywords#get-lines-matching-regexp) might be a better alternative here.

Otherwise let us know what do you want to achieve with `Find Text` and it looks overkill with the current keyword's capability. Also for increased visibility, please use our [forum](https://forum.robocorp.com/), so we can provide better support.
I will try to explain better with using logs.
I was checking "Find Text" with yours PDF which I found on GitHub (RPA.PDF folder somewhere) and  the KW works with this PDF (invoice.pdf)

![Find Text and Anchor Element RPA](https://user-images.githubusercontent.com/97021863/175520816-cf72ebdf-c408-4e81-8c8d-5f5e419adaaa.PNG)
.

But when I used this KW for basic format PDF (like a document) I got this log
![image](https://user-images.githubusercontent.com/97021863/175521446-ab0d6c6d-5a85-4f0c-b477-f3064d6bf62f.png)
I am not allowed to share the PDF with you but I am trying to find similar...

BTW Get Text From Pdf it has a mistake as well. The documentation meantions 
**pages: | page numbers to get text (numbers start from 0).**  I think it is not true and it starts with number one because my word was not found on the page zero but on the page 1. 

I would recommend to explore :)



Resolving patch:
diff --git a/packages/pdf/src/RPA/PDF/keywords/context.py b/packages/pdf/src/RPA/PDF/keywords/context.py
--- a/packages/pdf/src/RPA/PDF/keywords/context.py
+++ b/packages/pdf/src/RPA/PDF/keywords/context.py
@@ -27,3 +27,7 @@ def active_pdf_document(self):
     @active_pdf_document.setter
     def active_pdf_document(self, value):
         self.ctx.active_pdf_document = value
+
+    @property
+    def pages_count(self):
+        return self.ctx.get_number_of_pages()
diff --git a/packages/pdf/src/RPA/PDF/keywords/document.py b/packages/pdf/src/RPA/PDF/keywords/document.py
--- a/packages/pdf/src/RPA/PDF/keywords/document.py
+++ b/packages/pdf/src/RPA/PDF/keywords/document.py
@@ -20,7 +20,7 @@
 
 
 FilePath = Union[str, Path]
-ListOrString = Union[List[int], List[str], str, None]
+PagesType = Union[int, str, List[int], List[str], None]
 
 ASSETS_DIR = Path(__file__).resolve().parent.parent / "assets"
 
@@ -327,8 +327,8 @@ def get_pdf_metadata():
         self.switch_to_pdf(source_path)
 
         reader = self.active_pdf_document.reader
-        docinfo = reader.getDocumentInfo()
-        num_pages = reader.getNumPages()
+        docinfo = reader.metadata
+        num_pages = self.pages_count
 
         parser = PDFParser(self.active_pdf_document.fileobject)
         document = PDFDocument(parser)
@@ -424,7 +424,7 @@ def number_of_pages_in_pdf():
         """
         self.switch_to_pdf(source_path)
         reader = self.active_pdf_document.reader
-        return reader.getNumPages()
+        return len(reader.pages)
 
     @keyword
     def switch_to_pdf(self, source_path: Optional[FilePath] = None) -> None:
@@ -480,7 +480,7 @@ def jump_to_another_pdf():
     def get_text_from_pdf(
         self,
         source_path: str = None,
-        pages: ListOrString = None,
+        pages: PagesType = None,
         details: bool = False,
         trim: bool = True,
     ) -> dict:
@@ -514,7 +514,7 @@ def text_extraction_from_pdf():
 
 
         :param source_path: filepath to the source pdf.
-        :param pages: page numbers to get text (numbers start from 0).
+        :param pages: page numbers to get text (numbers start from 1).
         :param details: set to `True` to return textboxes, default `False`.
         :param trim: set to `False` to return raw texts, default `True`
             means whitespace is trimmed from the text
@@ -542,7 +542,7 @@ def extract_pages_from_pdf(
         self,
         source_path: str = None,
         output_path: str = None,
-        pages: ListOrString = None,
+        pages: PagesType = None,
     ) -> None:
         """Extract pages from source PDF and save to a new PDF document.
 
@@ -586,7 +586,7 @@ def save_pdf_pages_to_a_new_document():
         :param source_path: filepath to the source pdf.
         :param output_path: filepath to the target pdf, stored by default
             in the robot output directory as ``output.pdf``
-        :param pages: page numbers to extract from PDF (numbers start from 0)
+        :param pages: page numbers to extract from PDF (numbers start from 1)
             if None then extracts all pages.
         """
         self.switch_to_pdf(source_path)
@@ -595,18 +595,18 @@ def save_pdf_pages_to_a_new_document():
 
         output_path = self.resolve_output(output_path)
 
-        pages = self._get_page_numbers(pages, reader)
-        for pagenum in pages:
-            writer.addPage(reader.getPage(int(pagenum) - 1))
+        pages: List[int] = self._get_page_numbers(pages, reader)  # 1-indexed
+        for page_nr in pages:
+            writer.add_page(reader.pages[page_nr - 1])
         with open(output_path, "wb") as stream:
             writer.write(stream)
 
     @keyword
     def rotate_page(
         self,
-        pages: ListOrString,
-        source_path: str = None,
-        output_path: str = None,
+        pages: PagesType,
+        source_path: Optional[str] = None,
+        output_path: Optional[str] = None,
         clockwise: bool = True,
         angle: int = 90,
     ) -> None:
@@ -642,7 +642,7 @@ def pdf_page_rotation():
                     pages=5
                 )
 
-        :param pages: page numbers to extract from PDF (numbers start from 0).
+        :param pages: page numbers to extract from PDF (numbers start from 1).
         :param source_path: filepath to the source pdf.
         :param output_path: filepath to the target pdf, stored by default
             in the robot output directory as ``output.pdf``
@@ -657,18 +657,16 @@ def pdf_page_rotation():
         output_path = self.resolve_output(output_path)
 
         pages = self._get_page_numbers(pages, reader)
-        for page in range(reader.getNumPages()):
-            source_page = reader.getPage(int(page))
-            if page in pages:
-                if clockwise:
-                    source_page.rotateClockwise(int(angle))
-                else:
-                    source_page.rotateCounterClockwise(int(angle))
-            else:
-                source_page = reader.getPage(int(page))
-            writer.addPage(source_page)
-        with open(output_path, "wb") as f:
-            writer.write(f)
+        for page, source_page in enumerate(reader.pages):
+            if page + 1 in pages:
+                rotate_attr = (
+                    "rotateClockwise" if clockwise else "rotateCounterClockwise"
+                )
+                rotate_func = getattr(source_page, rotate_attr)
+                rotate_func(int(angle))
+            writer.add_page(source_page)
+        with open(output_path, "wb") as stream:
+            writer.write(stream)
 
     @keyword
     def encrypt_pdf(
@@ -886,7 +884,7 @@ def indicate_approved_with_watermark():
         input_reader = self.active_pdf_document.reader
 
         # Set image boundaries.
-        mediabox = input_reader.getPage(0).mediaBox
+        mediabox = input_reader.pages[0].mediaBox
         img_obj = Image.open(image_path)
         max_width = int(float(mediabox.getWidth()) * coverage)
         max_height = int(float(mediabox.getHeight()) * coverage)
@@ -908,14 +906,13 @@ def indicate_approved_with_watermark():
             # Get image page from temporary PDF using PyPDF2. (compatible with the
             # writer)
             img_pdf_reader = PyPDF2.PdfFileReader(temp_img_pdf)
-            watermark_page = img_pdf_reader.getPage(0)
+            watermark_page = img_pdf_reader.pages[0]
 
             # Write the merged pages of source PDF into the destination one.
             output_writer = PyPDF2.PdfFileWriter()
-            for idx in range(input_reader.getNumPages()):
-                page = input_reader.getPage(idx)
+            for page in input_reader.pages:
                 page.mergePage(watermark_page)
-                output_writer.addPage(page)
+                output_writer.add_page(page)
 
             # Since the input PDF can be the same with the output, make sure we close
             #  the input stream after writing into an auxiliary buffer. (if the input
@@ -982,10 +979,9 @@ def save_changes_to_pdf():
         :param reader: a PyPDF2 reader
         """
         writer = PyPDF2.PdfFileWriter()
-        for idx in range(reader.getNumPages()):
-            page = reader.getPage(idx)
+        for page in reader.pages:
             try:
-                writer.addPage(page)
+                writer.add_page(page)
             except Exception as exc:  # pylint: disable=W0703
                 self.logger.warning(repr(exc))
                 raise
@@ -996,11 +992,9 @@ def save_changes_to_pdf():
 
     @staticmethod
     def _get_page_numbers(
-        pages: ListOrString = None, reader: PyPDF2.PdfFileReader = None
+        pages: PagesType = None, reader: Optional[PyPDF2.PdfFileReader] = None
     ) -> List[int]:
-        """
-        Resolve page numbers argument to a list.
-        """
+        """Resolve page numbers argument to a list of 1-indexed integer pages."""
         if not pages and not reader:
             raise ValueError("Need a reader instance or explicit page numbers")
 
@@ -1009,7 +1003,7 @@ def _get_page_numbers(
         elif pages and isinstance(pages, int):
             pages = [pages]
         elif reader and not pages:
-            pages = range(1, reader.getNumPages() + 1)
+            pages = range(1, len(reader.pages) + 1)
 
         return list(map(int, pages))
 
@@ -1089,9 +1083,9 @@ def figure_to_image():
     @keyword
     def save_figures_as_images(
         self,
-        source_path: str = None,
+        source_path: Optional[str] = None,
         images_folder: str = ".",
-        pages: str = None,
+        pages: Optional[str] = None,
         file_prefix: str = "",
     ) -> List[str]:
         """Save figures from given PDF document as image files.
@@ -1131,7 +1125,8 @@ def figures_to_images():
         :param source_path: filepath to PDF document
         :param images_folder: directory where image files will be created
         :param pages: target figures in the pages, can be single page or range,
-         default `None` means that all pages are scanned for figures to save
+            default `None` means that all pages are scanned for figures to save
+            (numbers start from 1)
         :param file_prefix: image filename prefix
         :return: list of image filenames created
         """
@@ -1230,18 +1225,18 @@ def example_keyword():
             parameters = namesplit[1] if len(namesplit) == 2 else None
             file_to_add = file_to_add.parent / basename
             image_filetype = imghdr.what(str(file_to_add))
-            self.logger.info("File %s type: %s" % (str(file_to_add), image_filetype))
+            self.logger.info("File %s type: %s", str(file_to_add), image_filetype)
             if basename.lower().endswith(".pdf"):
                 reader = PyPDF2.PdfFileReader(str(file_to_add), strict=False)
-                pagecount = reader.getNumPages()
-                pages = self._get_pages(pagecount, parameters)
-                for n in pages:
+                pages = self._get_pages(len(reader.pages), parameters)
+                for page_nr in pages:
                     try:
-                        page = reader.getPage(n - 1)
-                        writer.addPage(page)
+                        # Because is 1-offset with `_get_pages()`.
+                        page = reader.pages[page_nr - 1]
+                        writer.add_page(page)
                     except IndexError:
                         self.logger.warning(
-                            "File %s does not have page %d" % (file_to_add, n)
+                            "File %s does not have page %d", file_to_add, page_nr
                         )
             elif image_filetype in ["png", "jpg", "jpeg", "gif"]:
                 temp_pdf = os.path.join(tempfile.gettempdir(), "temp.pdf")
@@ -1263,7 +1258,7 @@ def example_keyword():
                 pdf.output(name=temp_pdf)
 
                 reader = PyPDF2.PdfFileReader(temp_pdf)
-                writer.addPage(reader.getPage(0))
+                writer.add_page(reader.pages[0])
 
         with open(target_document, "wb") as f:
             writer.write(f)
@@ -1276,14 +1271,14 @@ def _add_pages_to_writer(self, writer, target_document):
             )
         else:
             reader = PyPDF2.PdfFileReader(str(target_document), strict=False)
-            pagecount = reader.getNumPages()
-            for n in range(pagecount):
-                page = reader.getPage(n)
-                self.logger.info("Adding page: %s" % n)
-                writer.addPage(page)
-
-    def _get_pages(self, pagecount, page_reference):
-        page_reference = f"1-{pagecount}" if page_reference is None else page_reference
+            for idx, page in enumerate(reader.pages):
+                self.logger.info("Adding page: %s", idx)
+                writer.add_page(page)
+
+    @staticmethod
+    def _get_pages(pagecount: int, page_reference: Optional[str]) -> List[int]:
+        """Returns a flattened list of pages based on provided 1-indexed ranges."""
+        page_reference = page_reference or f"1-{pagecount}"
         temp = [
             (lambda sub: range(sub[0], sub[-1] + 1))(
                 list(map(int, ele.strip().split("-")))
diff --git a/packages/pdf/src/RPA/PDF/keywords/model.py b/packages/pdf/src/RPA/PDF/keywords/model.py
--- a/packages/pdf/src/RPA/PDF/keywords/model.py
+++ b/packages/pdf/src/RPA/PDF/keywords/model.py
@@ -421,8 +421,8 @@ def convert(
 
         :param source_path: source PDF filepath
         :param trim: trim whitespace from the text is set to True (default)
-        :param pagenum: Page number where search is performed on, defaults to `None`. (
-            meaning all pages get converted)
+        :param pagenum: Page number where search is performed on, defaults to `None`.
+            (meaning all pages get converted -- numbers start from 1)
 
         **Examples**
 
@@ -455,7 +455,7 @@ def example_keyword():
             if pagenum in converted_pages:
                 return  # specific page already converted
         else:
-            pages_count = self.active_pdf_document.reader.getNumPages()
+            pages_count = self.pages_count
             if len(converted_pages) >= pages_count:
                 return  # all pages got converted already
 
@@ -753,14 +753,13 @@ def multiple_operations():
             self.logger.debug("No values available for updating the form fields")
             updated_fields = {}
 
-        for idx in range(reader.getNumPages()):
-            page = reader.getPage(idx)
+        for page in reader.pages:
             if updated_fields:
                 try:
                     writer.updatePageFormFieldValues(page, fields=updated_fields)
                 except Exception as exc:  # pylint: disable=W0703
                     self.logger.warning(repr(exc))
-            writer.addPage(page)
+            writer.add_page(page)
 
         if output_path is None:
             output_path = self.active_pdf_document.path


Test patch:
diff --git a/packages/pdf/tests/python/test_document.py b/packages/pdf/tests/python/test_document.py
--- a/packages/pdf/tests/python/test_document.py
+++ b/packages/pdf/tests/python/test_document.py
@@ -95,21 +95,48 @@ def test_html_to_pdf(library, text, encoding):
     assert text in result
 
 
-def test_rotate_page(library):
-    def get_source_page(pdf_file, page_num):
-        reader = PyPDF2.PdfFileReader(pdf_file)
-        return reader.getPage(int(page_num))
+def _get_source_pages(pdf_file, page_nums):
+    reader = PyPDF2.PdfFileReader(pdf_file)
+    if not page_nums:
+        return reader.pages
 
-    page_num_to_rotate = 1
-    page_before_rotation = get_source_page(str(TestFiles.vero_pdf), page_num_to_rotate)
+    return [reader.pages[page] for page in page_nums]
 
-    assert page_before_rotation["/Rotate"] == 0
 
-    with temp_filename() as tmp_file:
-        library.rotate_page(page_num_to_rotate, TestFiles.vero_pdf, tmp_file)
-        page_after_rotation = get_source_page(tmp_file, page_num_to_rotate)
-
-        assert page_after_rotation["/Rotate"] == 90
+@pytest.mark.parametrize(
+    "pages, internal_pages, clockwise",
+    [
+        (1, [0], True),
+        (1, [0], False),
+        (
+            0,
+            [0, 1],
+            True,
+        ),  # 0 is considered null, thus all pages are taken into account
+        ("1", [0], True),  # string index
+        ("1,2", [0, 1], False),  # string range
+        ("2", [1], True),  # string index, last page
+        (3, [], True),  # no pages to rotate, so all remain the same
+        ([2, 1], [0, 1], False),  # list of integers
+    ],
+)
+def test_rotate_page(library, pages, internal_pages, clockwise):
+    pages_before_rotation = _get_source_pages(str(TestFiles.vero_pdf), internal_pages)
+    before_obtained = [page["/Rotate"] for page in pages_before_rotation]
+    before_expected = [0] * (len(internal_pages) or len(pages_before_rotation))
+    assert before_obtained == before_expected
+
+    with temp_filename(suffix="-rotated.pdf") as tmp_file:
+        library.rotate_page(pages, TestFiles.vero_pdf, tmp_file, clockwise=clockwise)
+        pages_after_rotation = _get_source_pages(tmp_file, internal_pages)
+        after_obtained = [page["/Rotate"] for page in pages_after_rotation]
+        after_expected = (
+            [90 * (1 if clockwise else -1)] * len(internal_pages)
+            if internal_pages
+            else [0] * len(after_obtained)
+        )
+        assert after_expected  # ensuring we don't end up with an empty list
+        assert after_obtained == after_expected
 
 
 def test_encrypt_pdf(library):