From ce06352964ac4d6dce039b93181a54868290ef5c Mon Sep 17 00:00:00 2001 From: Jorj McKie Date: Wed, 5 May 2021 08:29:59 -0400 Subject: [PATCH] upload v1.18.13 --- PKG-INFO | 2 +- README.md | 2 +- changes.rst | 12 +-- docs/app1.rst | 2 +- docs/changes.rst | 42 +++++----- docs/coop_low.rst | 2 +- docs/deprecated.rst | 130 ++++++++++++++++++++++++++++++ docs/document.rst | 47 ++++++----- docs/faq.rst | 101 ++++++++++++----------- docs/functions.rst | 10 +-- docs/make-bold.py | 3 +- docs/multiprocess-gui.py | 10 +-- docs/multiprocess-render.py | 2 +- docs/new-annots.py | 28 +++---- docs/page.rst | 112 ++++++++++++------------- docs/pixmap.rst | 96 ++++++++++------------ docs/quad.rst | 2 +- docs/textpage.rst | 2 +- docs/tutorial.rst | 4 +- docs/version.rst | 2 +- fitz/__init__.py | 17 ++++ fitz/fitz.i | 101 ++++++++++++++--------- fitz/helper-select.i | 30 ++++--- fitz/utils.py | 54 +++++++++++-- fitz/version.i | 4 +- tests/resources/image-file1.pdf | Bin 0 -> 81019 bytes tests/test_badfonts.py | 2 +- tests/test_drawings.py | 7 +- tests/test_geometry.py | 3 +- tests/test_imagebbox.py | 33 ++++++++ tests/test_insertpdf.py | 23 ++++-- tests/test_linequad.py | 9 ++- tests/test_metadata.py | 10 ++- tests/test_nonpdf.py | 3 +- tests/test_object_manipulation.py | 21 ++++- tests/test_pagedelete.py | 62 ++++++++++++++ tests/test_pagelabels.py | 11 +++ tests/test_pixmap.py | 6 +- tests/test_showpdfpage.py | 26 +++--- tests/test_textbox.py | 3 +- 40 files changed, 701 insertions(+), 335 deletions(-) create mode 100644 tests/resources/image-file1.pdf create mode 100644 tests/test_imagebbox.py create mode 100644 tests/test_pagedelete.py diff --git a/PKG-INFO b/PKG-INFO index 2e05d14da..56b8d01de 100644 --- a/PKG-INFO +++ b/PKG-INFO @@ -10,7 +10,7 @@ Home-page: https://github.com/pymupdf/PyMuPDF Download-url: https://github.com/pymupdf/PyMuPDF Summary: PyMuPDF is a Python binding for the document renderer and toolkit MuPDF Description: - Release date: April 29, 2021 + Release date: May 5, 2021 Authors ======= diff --git a/README.md b/README.md index 0cbad4ffe..8285f31e8 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ ![logo](https://github.com/pymupdf/PyMuPDF/blob/master/demo/pymupdf.jpg) -Release date: April 30, 2021 +Release date: May 5, 2021 **Travis-CI:** [![Build Status](https://travis-ci.org/JorjMcKie/py-mupdf.svg?branch=master)](https://travis-ci.org/JorjMcKie/py-mupdf) diff --git a/changes.rst b/changes.rst index 7b25a4827..eb1228dd2 100644 --- a/changes.rst +++ b/changes.rst @@ -9,7 +9,9 @@ Changes in Version 1.18.13 * **Added** documentation for maintaining private entries in PDF metadata. * **Added** documentation for handling transparent image insertions, :meth:`Page.insert_image`. * **Added** :meth:`Page.get_image_rects`, an improved version of :meth:`Page.get_image_bbox`. +* **Changed** :meth:`Document.delete_pages` to support various ways of specifying pages to delete. Implements `#1042 `_. * **Changed** :meth:`Page.insert_image` to also accept the xref of an existing image in the file. This allows "copying" images between pages, and extremely fast mutiple insertions. +* **Changed** :meth:`Page.insert_image` to also accept the integer parameter ``alpha``. To be used for performance improvements. * **Changed** :meth:`Pixmap.set_alpha` to support new parameters for pre-multiplying colors with their alpha values and setting a specific color to fully transparent (e.g. white). * **Changed** :meth:`Document.embfile_add` to automatically set creation and modification date-time. Correspondingly, :meth:`Document.embfile_upd` automatically maintains modification date-time (``/ModDate`` PDF key), and :meth:`Document.embfile_info` correspondingly reports these data. In addition, the embedded file's associated "collection item" is included via its :data:`xref`. This supports the development of PDF portfolio applications. @@ -240,7 +242,7 @@ Changes in Version 1.17.3 * **Fixed** issue `#540 `_. Text extraction for EPUB should again work correctly. * **Fixed** issue `#548 `_. Documentation now includes ``LINK_NAMED``. * **Added** new parameter to control start of text in :meth:`TextWriter.fillTextbox`. Implements `#549 `_. -* **Changed** documentation of :meth:`Page.addRedactAnnot` to explain the usage of non-builtin fonts. +* **Changed** documentation of :meth:`Page.add_redact_annot` to explain the usage of non-builtin fonts. Changes in Version 1.17.2 --------------------------- @@ -270,7 +272,7 @@ Other changes: * **Changed** :meth:`TextWriter.writeText` to support the *"morph"* parameter. * **Added** methods :meth:`Rect.morph`, :meth:`IRect.morph`, and :meth:`Quad.morph`, which return a new :ref:`Quad`. -* **Changed** :meth:`Page.addFreetextAnnot` to support text alignment via a new *"align"* parameter. +* **Changed** :meth:`Page.add_freetext_annot` to support text alignment via a new *"align"* parameter. * **Fixed** issue `#508 `_. Improved image rectangle calculation to hopefully deliver correct values in most if not all cases. * **Fixed** issue `#502 `_. * **Fixed** issue `#500 `_. :meth:`Document.convertToPDF` should no longer cause memory leaks. @@ -348,7 +350,7 @@ Changes in Version 1.16.12 Changes in Version 1.16.11 --------------------------- -* **Added** Support for redaction annotations via method :meth:`Page.addRedactAnnot` and :meth:`Page.apply_redactions`. +* **Added** Support for redaction annotations via method :meth:`Page.add_redact_annot` and :meth:`Page.apply_redactions`. * **Fixed** issue #426 ("PolygonAnnotation in 1.16.10 version"). * **Fixed** documentation only issues `#443 `_ and `#444 `_. @@ -462,7 +464,7 @@ List of change details: * **Changed** font support for widgets: only *Cour* (Courier), *Helv* (Helvetica, default), *TiRo* (Times-Roman) and *ZaDb* (ZapfDingBats) are accepted when **adding or changing** form fields. Only the plain versions are possible -- not their italic or bold variations. **Reading** widgets, however will show its original font. * **Changed** the name of the warnings buffer to :meth:`Tools.mupdf_warnings` and the function to empty this buffer is now called :meth:`Tools.reset_mupdf_warnings`. * **Changed** :meth:`Page.getPixmap`, :meth:`Document.get_page_pixmap`: a new bool argument *annots* can now be used to **suppress the rendering of annotations** on the page. -* **Changed** :meth:`Page.addFileAnnot` and :meth:`Page.addTextAnnot` to enable setting an icon. +* **Changed** :meth:`Page.add_file_annot` and :meth:`Page.add_text_annot` to enable setting an icon. * **Removed** widget-related methods and attributes from the :ref:`Annot` object. * **Removed** :ref:`Document` attributes *openErrCode*, *openErrMsg*, and :ref:`Tools` attributes / methods *stderr*, *reset_stderr*, *stdout*, and *reset_stdout*. * **Removed** **thirdparty zlib** dependency in PyMuPDF: there are now compression functions available in MuPDF. Source installers of PyMuPDF may now omit this extra installation step. @@ -663,7 +665,7 @@ This patch version contains several improvements for embedded files and file att * **Changed** :meth:`Document.embfile_Add` to now automatically compress file content. Accompanying metadata can now be unicode (had to be ASCII in the past). * **Changed** :meth:`Document.embfile_Del` to now automatically delete **all entries** having the supplied identifying name. The return code is now an integer count of the removed entries (was *None* previously). * **Changed** embedded file methods to now also accept or show the PDF unicode filename as additional parameter *ufilename*. -* **Added** :meth:`Page.addFileAnnot` which adds a new file attachment annotation. +* **Added** :meth:`Page.add_file_annot` which adds a new file attachment annotation. * **Changed** :meth:`Annot.fileUpd` (file attachment annot) to now also accept the PDF unicode *ufilename* parameter. The description parameter *desc* correctly works with unicode. Furthermore, **all** parameters are optional, so metadata may be changed without also replacing the file content. * **Changed** :meth:`Annot.fileInfo` (file attachment annot) to now also show the PDF unicode filename as parameter *ufilename*. * **Fixed** issue #180 ("page.getText(output='dict') return invalid bbox") to now also work for vertical text. diff --git a/docs/app1.rst b/docs/app1.rst index 01608af11..c916eba18 100644 --- a/docs/app1.rst +++ b/docs/app1.rst @@ -142,7 +142,7 @@ We have tested rendering speed of MuPDF against the *pdftopng.exe*, a command li doc=fitz.open(datei) for p in fitz.Pages(doc): pix = p.get_pixmap(matrix=mat, alpha = False) - pix.writePNG("t-%s.png" % p.number) + pix.save("t-%s.png" % p.number) pix = None doc.close() return diff --git a/docs/changes.rst b/docs/changes.rst index a956b17ee..eb1228dd2 100644 --- a/docs/changes.rst +++ b/docs/changes.rst @@ -9,8 +9,10 @@ Changes in Version 1.18.13 * **Added** documentation for maintaining private entries in PDF metadata. * **Added** documentation for handling transparent image insertions, :meth:`Page.insert_image`. * **Added** :meth:`Page.get_image_rects`, an improved version of :meth:`Page.get_image_bbox`. +* **Changed** :meth:`Document.delete_pages` to support various ways of specifying pages to delete. Implements `#1042 `_. * **Changed** :meth:`Page.insert_image` to also accept the xref of an existing image in the file. This allows "copying" images between pages, and extremely fast mutiple insertions. -* **Changed** :meth:`Pixmap.setAlpha` to support new parameters for pre-multiplying colors with their alpha values and setting a specific color to fully transparent (e.g. white). +* **Changed** :meth:`Page.insert_image` to also accept the integer parameter ``alpha``. To be used for performance improvements. +* **Changed** :meth:`Pixmap.set_alpha` to support new parameters for pre-multiplying colors with their alpha values and setting a specific color to fully transparent (e.g. white). * **Changed** :meth:`Document.embfile_add` to automatically set creation and modification date-time. Correspondingly, :meth:`Document.embfile_upd` automatically maintains modification date-time (``/ModDate`` PDF key), and :meth:`Document.embfile_info` correspondingly reports these data. In addition, the embedded file's associated "collection item" is included via its :data:`xref`. This supports the development of PDF portfolio applications. Changes in Version 1.18.11 / 1.18.12 @@ -194,7 +196,7 @@ This is the first PyMuPDF version supporting MuPDF v1.18. The focus here is on e Changes in Version 1.17.7 --------------------------- * **Fixed** issue `#651 `_. An upstream bug causing interpreter crashes in corner case redaction processings was fixed by backporting MuPDF changes from their development repo. -* **Fixed** issue `#645 `_. Pixmap top-left coordinates can be set (again) by their own method, :meth:`Pixmap.setOrigin`. +* **Fixed** issue `#645 `_. Pixmap top-left coordinates can be set (again) by their own method, :meth:`Pixmap.set_origin`. * **Fixed** issue `#622 `_. :meth:`Page.insertImage` again accepts a :data:`rect_like` parameter. * **Added** severeal new methods to improve and speed-up table of contents (TOC) handling. Among other things, TOC items can now changed or deleted individually -- without always replacing the complete TOC. Furthermore, access to some PDF page attributes is now possible without first **loading** the page. This has a very significant impact on the performance of TOC manipulation. * **Added** an option to :meth:`Document.insert_pdf` which allows displaying progress messages. Adresses `#640 `_. @@ -240,7 +242,7 @@ Changes in Version 1.17.3 * **Fixed** issue `#540 `_. Text extraction for EPUB should again work correctly. * **Fixed** issue `#548 `_. Documentation now includes ``LINK_NAMED``. * **Added** new parameter to control start of text in :meth:`TextWriter.fillTextbox`. Implements `#549 `_. -* **Changed** documentation of :meth:`Page.addRedactAnnot` to explain the usage of non-builtin fonts. +* **Changed** documentation of :meth:`Page.add_redact_annot` to explain the usage of non-builtin fonts. Changes in Version 1.17.2 --------------------------- @@ -270,7 +272,7 @@ Other changes: * **Changed** :meth:`TextWriter.writeText` to support the *"morph"* parameter. * **Added** methods :meth:`Rect.morph`, :meth:`IRect.morph`, and :meth:`Quad.morph`, which return a new :ref:`Quad`. -* **Changed** :meth:`Page.addFreetextAnnot` to support text alignment via a new *"align"* parameter. +* **Changed** :meth:`Page.add_freetext_annot` to support text alignment via a new *"align"* parameter. * **Fixed** issue `#508 `_. Improved image rectangle calculation to hopefully deliver correct values in most if not all cases. * **Fixed** issue `#502 `_. * **Fixed** issue `#500 `_. :meth:`Document.convertToPDF` should no longer cause memory leaks. @@ -299,7 +301,7 @@ Changes in Version 1.16.17 --------------------------- * **Fixed** issue `#479 `_. PyMuPDF should now more correctly report image resolutions. This applies to both, images (either from images files or extracted from PDF documents) and pixmaps created from images. -* **Added** :meth:`Pixmap.setResolution` which sets the image resolution in x and y directions. +* **Added** :meth:`Pixmap.set_dpi` which sets the image resolution in x and y directions. Changes in Version 1.16.16 --------------------------- @@ -348,13 +350,13 @@ Changes in Version 1.16.12 Changes in Version 1.16.11 --------------------------- -* **Added** Support for redaction annotations via method :meth:`Page.addRedactAnnot` and :meth:`Page.apply_redactions`. +* **Added** Support for redaction annotations via method :meth:`Page.add_redact_annot` and :meth:`Page.apply_redactions`. * **Fixed** issue #426 ("PolygonAnnotation in 1.16.10 version"). * **Fixed** documentation only issues `#443 `_ and `#444 `_. Changes in Version 1.16.10 --------------------------- -* **Fixed** issue #421 ("annot.setRect(rect) has no effect on text Annotation") +* **Fixed** issue #421 ("annot.set_rect(rect) has no effect on text Annotation") * **Fixed** issue #417 ("Strange behavior for page.deleteAnnot on 1.16.9 compare to 1.13.20") * **Fixed** issue #415 ("Annot.setOpacity throws mupdf warnings") * **Changed** all "add annotation / widget" methods to store a unique name in the */NM* PDF key. @@ -462,7 +464,7 @@ List of change details: * **Changed** font support for widgets: only *Cour* (Courier), *Helv* (Helvetica, default), *TiRo* (Times-Roman) and *ZaDb* (ZapfDingBats) are accepted when **adding or changing** form fields. Only the plain versions are possible -- not their italic or bold variations. **Reading** widgets, however will show its original font. * **Changed** the name of the warnings buffer to :meth:`Tools.mupdf_warnings` and the function to empty this buffer is now called :meth:`Tools.reset_mupdf_warnings`. * **Changed** :meth:`Page.getPixmap`, :meth:`Document.get_page_pixmap`: a new bool argument *annots* can now be used to **suppress the rendering of annotations** on the page. -* **Changed** :meth:`Page.addFileAnnot` and :meth:`Page.addTextAnnot` to enable setting an icon. +* **Changed** :meth:`Page.add_file_annot` and :meth:`Page.add_text_annot` to enable setting an icon. * **Removed** widget-related methods and attributes from the :ref:`Annot` object. * **Removed** :ref:`Document` attributes *openErrCode*, *openErrMsg*, and :ref:`Tools` attributes / methods *stderr*, *reset_stderr*, *stdout*, and *reset_stdout*. * **Removed** **thirdparty zlib** dependency in PyMuPDF: there are now compression functions available in MuPDF. Source installers of PyMuPDF may now omit this extra installation step. @@ -542,27 +544,27 @@ Changes in Version 1.14.9 Changes in Version 1.14.8 --------------------------- -* **Added** :meth:`Pixmap.setRect` to change the pixel values in a rectangle. This is also an alternative to setting the color of a complete pixmap (:meth:`Pixmap.clearWith`). +* **Added** :meth:`Pixmap.set_rect` to change the pixel values in a rectangle. This is also an alternative to setting the color of a complete pixmap (:meth:`Pixmap.clear_with`). * **Fixed** an image extraction issue with JBIG2 (monochrome) encoded PDF images. The issue occurred in :meth:`Page.getText` (parameters "dict" and "rawdict") and in :meth:`Document.extractImage` methods. -* **Fixed** an issue with not correctly clearing a non-alpha :ref:`Pixmap` (:meth:`Pixmap.clearWith`). -* **Fixed** an issue with not correctly inverting colors of a non-alpha :ref:`Pixmap` (:meth:`Pixmap.invertIRect`). +* **Fixed** an issue with not correctly clearing a non-alpha :ref:`Pixmap` (:meth:`Pixmap.clear_with`). +* **Fixed** an issue with not correctly inverting colors of a non-alpha :ref:`Pixmap` (:meth:`Pixmap.invert_irect`). Changes in Version 1.14.7 --------------------------- -* **Added** :meth:`Pixmap.setPixel` to change one pixel value. +* **Added** :meth:`Pixmap.set_pixel` to change one pixel value. * **Added** documentation for image conversion in the :ref:`FAQ`. * **Added** new function :meth:`getTextlength` to determine the string length for a given font. -* **Added** Postscript image output (changed :meth:`Pixmap.writeImage` and :meth:`Pixmap.getImageData`). -* **Changed** :meth:`Pixmap.writeImage` and :meth:`Pixmap.getImageData` to ensure valid combinations of colorspace, alpha and output format. -* **Changed** :meth:`Pixmap.writeImage`: the desired format is now inferred from the filename. +* **Added** Postscript image output (changed :meth:`Pixmap.save` and :meth:`Pixmap.tobytes`). +* **Changed** :meth:`Pixmap.save` and :meth:`Pixmap.tobytes` to ensure valid combinations of colorspace, alpha and output format. +* **Changed** :meth:`Pixmap.save`: the desired format is now inferred from the filename. * **Changed** FreeText annotations can now have a transparent background - see :meth:`Annot.update`. Changes in Version 1.14.5 --------------------------- * **Changed:** :ref:`Shape` methods now strictly use the transformation matrix of the :ref:`Page` -- instead of "manually" calculating locations. * **Added** method :meth:`Pixmap.pixel` which returns the pixel value (a list) for given pixel coordinates. -* **Added** method :meth:`Pixmap.getImageData` which returns a bytes object representing the pixmap in a variety of formats. Previously, this could be done for PNG outputs only (:meth:`Pixmap.getPNGData`). -* **Changed:** output of methods :meth:`Pixmap.writeImage` and (the new) :meth:`Pixmap.getImageData` may now also be PSD (Adobe Photoshop Document). +* **Added** method :meth:`Pixmap.tobytes` which returns a bytes object representing the pixmap in a variety of formats. Previously, this could be done for PNG outputs only (:meth:`Pixmap.tobytes`). +* **Changed:** output of methods :meth:`Pixmap.save` and (the new) :meth:`Pixmap.tobytes` may now also be PSD (Adobe Photoshop Document). * **Added** method :meth:`Shape.drawQuad` which draws a :ref:`Quad`. This actually is a shorthand for a :meth:`Shape.drawPolyline` with the edges of the quad. * **Changed** method :meth:`Shape.drawOval`: the argument can now be **either** a rectangle (:data:`rect_like`) **or** a quadrilateral (:data:`quad_like`). @@ -663,7 +665,7 @@ This patch version contains several improvements for embedded files and file att * **Changed** :meth:`Document.embfile_Add` to now automatically compress file content. Accompanying metadata can now be unicode (had to be ASCII in the past). * **Changed** :meth:`Document.embfile_Del` to now automatically delete **all entries** having the supplied identifying name. The return code is now an integer count of the removed entries (was *None* previously). * **Changed** embedded file methods to now also accept or show the PDF unicode filename as additional parameter *ufilename*. -* **Added** :meth:`Page.addFileAnnot` which adds a new file attachment annotation. +* **Added** :meth:`Page.add_file_annot` which adds a new file attachment annotation. * **Changed** :meth:`Annot.fileUpd` (file attachment annot) to now also accept the PDF unicode *ufilename* parameter. The description parameter *desc* correctly works with unicode. Furthermore, **all** parameters are optional, so metadata may be changed without also replacing the file content. * **Changed** :meth:`Annot.fileInfo` (file attachment annot) to now also show the PDF unicode filename as parameter *ufilename*. * **Fixed** issue #180 ("page.getText(output='dict') return invalid bbox") to now also work for vertical text. @@ -760,7 +762,7 @@ Changes in Version 1.12.3 -------------------------- This is an extension of 1.12.2. -* Many functions now return *None* instead of *0*, if the result has no other meaning than just indicating successful execution (:meth:`Document.close`, :meth:`Document.save`, :meth:`Document.select`, :meth:`Pixmap.writePNG` and many others). +* Many functions now return *None* instead of *0*, if the result has no other meaning than just indicating successful execution (:meth:`Document.close`, :meth:`Document.save`, :meth:`Document.select`, :meth:`Pixmap.save` and many others). Changes in Version 1.12.2 -------------------------- @@ -958,7 +960,7 @@ Changes in version 1.9.1 compared to version 1.8.0 are the following: * The new document method *select(list)* removes all pages from a document that are not contained in the list. Pages can also be duplicated and re-arranged. * Various improvements and new members in our demo and examples collections. Perhaps most prominently: *PDF_display* now supports scrolling with the mouse wheel, and there is a new example program *wxTableExtract* which allows to graphically identify and extract table data in documents. * *fitz.open()* is now an alias of *fitz.Document()*. -* New pixmap method *getPNGData()* which will return a bytearray formatted as a PNG image of the pixmap. +* New pixmap method *tobytes()* which will return a bytearray formatted as a PNG image of the pixmap. * New pixmap method *samplesRGB()* providing a *samples* version with alpha bytes stripped off (RGB colorspaces only). * New pixmap method *samplesAlpha()* providing the alpha bytes only of the *samples* area. * New iterator *fitz.Pages(doc)* over a document's set of pages. diff --git a/docs/coop_low.rst b/docs/coop_low.rst index ac4aaacd5..21ee968b3 100644 --- a/docs/coop_low.rst +++ b/docs/coop_low.rst @@ -35,7 +35,7 @@ For this we need to create a :ref:`TextPage`. >>> tp = dl.get_textpage() # display list from above >>> rlist = tp.search("needle") # look up "needle" locations >>> for r in rlist: # work with the found locations, e.g. - pix.invertIRect(r.irect) # invert colors in the rectangles + pix.invert_irect(r.irect) # invert colors in the rectangles Extract Text ---------------- diff --git a/docs/deprecated.rst b/docs/deprecated.rst index 9add7759c..675ac9343 100644 --- a/docs/deprecated.rst +++ b/docs/deprecated.rst @@ -26,6 +26,74 @@ :attr:`Page.is_wrapped` +.. data:: addCaretAnnot + + :meth:`Page.add_caret_annot` + +.. data:: addCircleAnnot + + :meth:`Page.add_circle_annot` + +.. data:: addFileAnnot + + :meth:`Page.add_file_annot` + +.. data:: addFreetextAnnot + + :meth:`Page.add_freetext_annot` + +.. data:: addHighlightAnnot + + :meth:`Page.add_highlight_annot` + +.. data:: addInkAnnot + + :meth:`Page.add_ink_annot` + +.. data:: addLineAnnot + + :meth:`Page.add_line_annot` + +.. data:: addPolygonAnnot + + :meth:`Page.add_polygon_annot` + +.. data:: addPolylineAnnot + + :meth:`Page.add_polyline_annot` + +.. data:: addRectAnnot + + :meth:`Page.add_rect_annot` + +.. data:: addRedactAnnot + + :meth:`Page.add_redact_annot` + +.. data:: addSquigglyAnnot + + :meth:`Page.add_squiggly_annot` + +.. data:: addStampAnnot + + :meth:`Page.add_stamp_annot` + +.. data:: addStrikeoutAnnot + + :meth:`Page.add_strikeout_annot` + +.. data:: addTextAnnot + + :meth:`Page.add_text_annot` + +.. data:: addUnderlineAnnot + + :meth:`Page.add_underline_annot` + +.. data:: addWidget + + :meth:`Page.add_widget` + .. data:: chapterCount :attr:`Document.chapter_count` @@ -38,6 +106,10 @@ :meth:`Page.clean_contents` +.. data:: clearWith + + :meth:`Pixmap.clear_with` + .. data:: convertToPDF :meth:`Document.convert_to_pdf` @@ -46,6 +118,10 @@ :meth:`Document.copy_page` +.. data:: copyPixmap + + :meth:`Pixmap.copy` + .. data:: deleteAnnot :meth:`Page.delete_annot` @@ -204,6 +280,10 @@ :meth:`Document.fullcopy_page` +.. data:: gammaWith + + :meth:`Pixmap.gamma_with` + .. data:: getCharWidths :meth:`Document.get_char_widths` @@ -228,6 +308,10 @@ :meth:`Page.get_image_bbox` +.. data:: getImageData + + :meth:`Pixmap.tobytes` + .. data:: getImageList :meth:`Page.get_images` @@ -240,6 +324,10 @@ :meth:`Document.get_ocgs` +.. data:: getPNGData + + :meth:`Pixmap.tobytes` + .. data:: getPageFontList :meth:`Document.get_page_fonts` @@ -344,6 +432,10 @@ :meth:`Shape.insert_textbox` +.. data:: invertIRect + + :meth:`Pixmap.invert_irect` + .. data:: isDirty :attr:`Document.is_dirty` @@ -428,6 +520,14 @@ :meth:`Document.page_xref` +.. data:: pillowData + + :meth:`Pixmap.pil_tobytes` + +.. data:: pillowWrite + + :meth:`Pixmap.pil_save` + .. data:: previousLocation :meth:`Document.prev_location` @@ -452,6 +552,10 @@ :meth:`Document.search_page_for` +.. data:: setAlpha + + :meth:`Pixmap.set_alpha` + .. data:: setBlendMode :meth:`Annot.set_blendmode` @@ -504,10 +608,24 @@ :meth:`Annot.set_opacity` +.. data:: setOrigin + + :meth:`Pixmap.set_origin` + +.. data:: setPixel + + :meth:`Pixmap.set_pixel` + .. data:: setRect :meth:`Annot.set_rect` + :meth:`Pixmap.set_rect` + +.. data:: setResolution + + :meth:`Pixmap.set_dpi` + .. data:: setRotation :meth:`Page.set_rotation` @@ -528,6 +646,10 @@ :meth:`Annot.get_sound` +.. data:: tintWith + + :meth:`Pixmap.tint_with` + .. data:: transformationMatrix :attr:`Page.transformation_matrix` @@ -548,6 +670,14 @@ :meth:`Page.wrap_contents` +.. data:: writeImage + + :meth:`Pixmap.save` + +.. data:: writePNG + + :meth:`Pixmap.save` + .. data:: writeText :meth:`Page.write_text` diff --git a/docs/document.rst b/docs/document.rst index 1aa3dcad6..92a745534 100644 --- a/docs/document.rst +++ b/docs/document.rst @@ -35,7 +35,7 @@ For details on **embedded files** refer to Appendix 3. :meth:`Document.copy_page` PDF only: copy a page reference :meth:`Document.del_toc_item` PDF only: remove a single TOC item :meth:`Document.delete_page` PDF only: delete a page -:meth:`Document.delete_pages` PDF only: delete a page range +:meth:`Document.delete_pages` PDF only: delete multiple pages :meth:`Document.embfile_add` PDF only: add a new embedded file from buffer :meth:`Document.embfile_count` PDF only: number of embedded files :meth:`Document.embfile_del` PDF only: delete an embedded file entry @@ -600,7 +600,7 @@ For details on **embedded files** refer to Appendix 3. *(New in version 1.17.7)* - PDF only: Return the :data:`xref` of the page -- **without reading the page (via :meth:`Document.load_page`). This is meant for internal purpose requiring best possible performance. + PDF only: Return the :data:`xref` of the page -- **without reading the page** (via :meth:`Document.load_page`). This is meant for internal purpose requiring best possible performance. :arg int pno: 0-based page number. @@ -1211,37 +1211,36 @@ For details on **embedded files** refer to Appendix 3. :arg int pno: the page to be deleted. Negative number count backwards from the end of the document (like with indices). Default is the last page. - .. method:: delete_pages(from_page=-1, to_page=-1) + .. method:: delete_pages(*args, **kwds) - PDF only: Delete a range of pages given as 0-based numbers. Any *-1* parameter will first be replaced by *doc.page_count - 1* (ie. last page number). After that, condition *0 <= from_page <= to_page < doc.page_count* must be true. If the parameters are equal, this is equivalent to :meth:`delete_page`. + *Changed in v1.18.13: more flexibility specifying pages to delete.* - :arg int from_page: the first page to be deleted. + PDF only: Delete multiple pages given as 0-based numbers. *Changed in v1.18.13:* introduced much more flexibility for specifying pages. - :arg int to_page: the last page to be deleted. + **Format 1:** Use keywords. Represents the old format. + * "from_page": first page to delete. Zero if omitted. + * "to_page": last page to delete. Last page in document if omitted. Must not be less then "from_page". - .. note:: - - *(Changed in v1.14.17, optimized in v1.17.7)* In an effort to maintain a valid PDF structure, this method and :meth:`delete_page` will also invalidate items in the table of contents which happen to point to deleted pages. "Invalidation" here means, that the bookmark will point to nowhere and the title will show the string "<>". So the overall TOC structure is left intact. + **Format 2:** A sequence as one positional parameter. A list, tuple or range object specifying pages to delete. Pages need not be consecutive. - Similarly, it will remove any **links on remaining pages** that point to a deleted page. This action may have an extended response time for documents with many pages. + **Format 3:** Page number as a single positional parameter. Equivalent to :meth:`Page.delete_page`. - Example: Delete the page range 500 to 520 from a large PDF, using different methods. + **Format 4:** Two page numbers as positional parameters. Handled like Format 1. - Method 1 - *delete_pages*:: + .. note:: - import time, fitz - doc = fitz.open("Adobe PDF Reference 1-7.pdf") - t0=time.perf_counter();doc.delete_pages(500, 520);t1=time.perf_counter() - round(t1 - t0, 2) - 0.66 + *(Changed in v1.14.17, optimized in v1.17.7)* In an effort to maintain a valid PDF structure, this method and :meth:`delete_page` will also invalidate items in the table of contents which happen to point to deleted pages. "Invalidation" here means, that the bookmark will point to nowhere and the title will show the string "<>". The overall TOC structure is left intact. + Similarly, it will remove any **links on remaining pages** that point to a deleted one. This action may have an extended response time for documents with many pages. - Method 2 - *select*, this is more than 10 times **slower**:: + Following examples all delete pages 500 through 519: + + * ``doc.delete_pages(500, 519)`` + * ``doc.delete_pages(from_page=500, to_page=519)`` + * ``doc.delete_pages([500, 501, 502, ... , 519])`` + * ``doc.delete_pages(range(500, 520))`` - l = list(range(500)) + list(range(521, 1310)) - t0=time.perf_counter();doc.select(l);t1=time.perf_counter() - round(t1 - t0, 2) - 7.62 + For the :ref:`AdobeManual` the above takes about 0.5 to 0.6 seconds, because on every of the remaining 1290 pages all links must be removed, which point to a deleted pages. .. method:: copy_page(pno, to=-1) @@ -1736,10 +1735,10 @@ Other Examples xref = img[0] # xref number pix = fitz.Pixmap(doc, xref) # make pixmap from image if pix.n - pix.alpha < 4: # can be saved as PNG - pix.writePNG("p%s-%s.png" % (i, xref)) + pix.save("p%s-%s.png" % (i, xref)) else: # CMYK: must convert first pix0 = fitz.Pixmap(fitz.csRGB, pix) - pix0.writePNG("p%s-%s.png" % (i, xref)) + pix0.save("p%s-%s.png" % (i, xref)) pix0 = None # free Pixmap resources pix = None # free Pixmap resources diff --git a/docs/faq.rst b/docs/faq.rst index b522806a5..b27578a45 100644 --- a/docs/faq.rst +++ b/docs/faq.rst @@ -29,7 +29,7 @@ The script works as a command line tool which expects the filename being supplie doc = fitz.open(fname) # open document for page in doc: # iterate through the pages pix = page.get_pixmap(alpha = False) # render page to an image - pix.writePNG("page-%i.png" % page.number) # store image as a PNG + pix.save("page-%i.png" % page.number) # store image as a PNG The script directory will now contain PNG image files named *page-0.png*, *page-1.png*, etc. Pictures have the dimension of their pages, e.g. 595 x 842 pixels for an A4 portrait sized page. They will have a resolution of 72 dpi in x and y dimension and have no transparency. You can change all that -- for how to do this, read the next sections. @@ -127,7 +127,7 @@ Like any other "object" in a PDF, images are identified by a cross reference num 1. **Create** a :ref:`Pixmap` of the image with instruction *pix = fitz.Pixmap(doc, xref)*. This method is **very** fast (single digit micro-seconds). The pixmap's properties (width, height, ...) will reflect the ones of the image. In this case there is no way to tell which image format the embedded original has. -2. **Extract** the image with *img = doc.extract_image(xref)*. This is a dictionary containing the binary image data as *img["image"]*. A number of meta data are also provided -- mostly the same as you would find in the pixmap of the image. The major difference is string *img["ext"]*, which specifies the image format: apart from "png", strings like "jpeg", "bmp", "tiff", etc. can also occur. Use this string as the file extension if you want to store to disk. The execution speed of this method should be compared to the combined speed of the statements *pix = fitz.Pixmap(doc, xref);pix.getPNGData()*. If the embedded image is in PNG format, the speed of :meth:`Document.extract_image` is about the same (and the binary image data are identical). Otherwise, this method is **thousands of times faster**, and the **image data is much smaller**. +2. **Extract** the image with *img = doc.extract_image(xref)*. This is a dictionary containing the binary image data as *img["image"]*. A number of meta data are also provided -- mostly the same as you would find in the pixmap of the image. The major difference is string *img["ext"]*, which specifies the image format: apart from "png", strings like "jpeg", "bmp", "tiff", etc. can also occur. Use this string as the file extension if you want to store to disk. The execution speed of this method should be compared to the combined speed of the statements *pix = fitz.Pixmap(doc, xref);pix.tobytes()*. If the embedded image is in PNG format, the speed of :meth:`Document.extract_image` is about the same (and the binary image data are identical). Otherwise, this method is **thousands of times faster**, and the **image data is much smaller**. The question remains: **"How do I know those 'xref' numbers of images?"**. There are two answers to this: @@ -167,7 +167,7 @@ To recover the original image using PyMuPDF, the procedure depicted as follows m >>> pix1 = fitz.Pixmap(doc, xref) # (1) pixmap of image w/o alpha >>> pix2 = fitz.Pixmap(doc, smask) # (2) stencil pixmap >>> pix = fitz.Pixmap(pix1) # (3) copy of pix1, empty alpha channel added ->>> pix.setAlpha(pix2.samples) # (4) fill alpha channel +>>> pix.set_alpha(pix2.samples) # (4) fill alpha channel Step (1) creates a pixmap of the "netto" image. Step (2) does the same with the stencil mask. Please note that the :attr:`Pixmap.samples` attribute of *pix2* contains the alpha bytes that must be stored in the final pixmap. This is what happens in step (3) and (4). @@ -180,7 +180,7 @@ The scripts `extract-imga.py = 3: # recursing needed? punch(x+i*s, y+j*s, s) # recurse else: # punching alternatives are: - pm.setRect((x+s, y+s, x+2*s, y+2*s), color) # fill with a color - #pm.copyPixmap(fill, (x+s, y+s, x+2*s, y+2*s)) # copy from fill - #pm.invertIRect((x+s, y+s, x+2*s, y+2*s)) # invert colors + pm.set_rect((x+s, y+s, x+2*s, y+2*s), color) # fill with a color + #pm.copy(fill, (x+s, y+s, x+2*s, y+2*s)) # copy from fill + #pm.invert_irect((x+s, y+s, x+2*s, y+2*s)) # invert colors return @@ -462,7 +462,7 @@ This script creates a approximate image of it as a PNG, by going down to one-pix # now start punching holes into the pixmap punch(0, 0, d) t1 = time.perf_counter() - pm.writeImage("sierpinski-punch.png") + pm.save("sierpinski-punch.png") t2 = time.perf_counter() print ("%g sec to create / fill the pixmap" % round(t1-t0,3)) print ("%g sec to save the image" % round(t2-t1,3)) @@ -495,7 +495,7 @@ This shows how to create a PNG file from a numpy array (several times faster tha samples = bytearray(bild.tostring()) # get plain pixel data from numpy array pix = fitz.Pixmap(fitz.csRGB, width, height, samples, alpha=False) - pix.writePNG("test.png") + pix.save("test.png") ---------- @@ -559,24 +559,29 @@ How to Control the Size of Inserted Images For the following discussion, please also consult the previous section. -If the ``pixmap`` parameter is used in :meth:`Page.insert_image`, the image format stored in the PDF is **always PNG**. This is independent from in which way the pixmap has originally been created. +If the ``pixmap`` parameter is used in :meth:`Page.insert_image`, the image is always **stored in uncompressed PNG format**. This is independent from in which way the pixmap has originally been created. -For ``filename`` and ``stream`` parameters, the **original image format**, quality and size are preserved (JPEG, BMP, JPEG2000, etc.). **However:** if :meth:`Page.insert_image` detects an alpha channel, then the image is internally converted to a pixmap, which is then inserted instead -- obviously as a PNG, so the original format is lost. This may lead to an increased image size (even after compression), or be otherwise undesireable. +For ``filename`` and ``stream`` parameters, the **original image format, quality and size** are preserved (JPEG, BMP, JPEG2000, etc.). **However:** the method takes the following actions: -Here is a way to prevent this from happening and take a more direct control over the result. +1. Create an internal pixmap to see if the image is transparent. +2. If not transparent, discard pixmap and insert image in original format. +3. If transparent, create a new internal image and an image mask containing transparency information -- both in pixmap format -- and store both pixmap images. This will be **uncompressed PNG format** again. -* Use ``filename`` or ``stream`` whenever possible. If there is no alpha channel, the original image will be inserted as is. -* If possible, provide a transparent image as two separate binary (``bytes``) objects, by using both parameters ``stream=baseimage, mask=alphaimage``. This will store both parts in their original formats and prevent internal pixmap generation. -* Check whether your file or stream has an alpha channel via ``fitz.Pixmap(filename).alpha == 1`` or looking at ``img.mode`` for a PIL image. If ``img.mode.endswith("A")`` (e.g. "RGBA"), then an alpha channel is present. Split the image up into base image and transparency mask like this:: - +Here is what you can do to take a closer control: + +1. Often you **know already** before, whether an image is transparent. For example, if you have a PIL image, check the last letter of ``img.mode``. If you see "RGBA" you have an RGB image with an alpha channel. +2. If your image is not transparent, include ``alpha=0`` in your method arguments. The method will then skip internal pixmap creation and store the image as is. +3. If your image has alpha, you can use the following snippet to create two sub-images: (1) the base-image, (2) the mask image (alpha values). Then insert them combined using the ``stream`` and ``mask`` arguments. Again, the method will omit any alpha-checking or conversion and store image and mask as is:: + + # example: 'stream' contains a transparent PNG image: pix = fitz.Pixmap(stream) # intermediate pixmap - pix0 = fitz.Pixmap(pix, 0) # extract base image without alpha - baseimage = pix0.pillowData("FORMAT") # best use original image format - pix1 = fitz.Pixmap(None, pix) # extract alpha channel to make mask image - pixmask = fitz.Pixmap(fitz.csGRAY, pix1.width, pix1.height, pix1.samples, 0) - page.insert_image(rect, stream=baseimage, mask=pixmask.getPNGData()) + base = fitz.Pixmap(pix, 0) # extract base image without alpha + mask = fitz.Pixmap(None, pix) # extract alpha channel for the mask image + basestream = base.pil_tobytes("JPEG") + maskstream = mask.pil_tobytes("JPEG") + page.insert_image(rect, stream=basestream, mask=maskstream) -* In a similar way, you can **provide an alpha channel** for intransparent images to store them with a desired opacity:: +You can also use this technique to **add transparency** to an image:: stream = open("example.jpg", "rb").read() basepix = fitz.Pixmap(stream) @@ -585,7 +590,7 @@ Here is a way to prevent this from happening and take a more direct control over alphas = [value] * (basepix.width * basepix.height) alphas = bytearray(alphas) # convert to a bytearray pixmask = fitz.Pixmap(fitz.csGRAY, basepix.width, basepix.height, alphas, 0) - page.insert_image(rect, stream=stream, mask=pixmask.getPNGData()) + page.insert_image(rect, stream=stream, mask=pixmask.tobytes()) Text @@ -787,7 +792,7 @@ But you also have other options:: if text in w[4]: # w[4] is the word's string found += 1 # count r = fitz.Rect(w[:4]) # make rect from word bbox - page.addUnderlineAnnot(r) # underline + page.add_underline_annot(r) # underline return found fname = sys.argv[1] # filename @@ -842,7 +847,7 @@ This script searches for text and marks it:: rl = page.search_for(t, quads = True) # mark all found quads with one annotation - page.addSquigglyAnnot(rl) + page.add_squiggly_annot(rl) # save to a new PDF doc.save("a-squiggly.pdf") @@ -863,12 +868,12 @@ But text **extraction** with the "dict" / "rawdict" options of :meth:`Page.get_t The "bboxes" returned by the method however are rectangles only -- not quads. So, to mark span text correctly, its quad must be recovered from the data contained in the line and span dictionary. Do this with the following utility function (new in v1.18.9):: span_quad = fitz.recover_quad(line["dir"], span) - annot = page.addHighlightAnnot(span_quad) # this will mark the complete span text + annot = page.add_highlight_annot(span_quad) # this will mark the complete span text If you want to **mark the complete line** or a subset of its spans in one go, use the following snippet (works for v1.18.10 or later):: line_quad = fitz.recover_line_quad(line, spans=line["spans"][1:-1]) - page.addHighlightAnnot(line_quad) + page.add_highlight_annot(line_quad) .. image:: images/img-linequad.* @@ -1136,9 +1141,9 @@ This script shows a couple of ways to deal with 'FreeText' annotations:: t = "¡Un pequeño texto para practicar!" # add 3 annots, modify the last one somewhat - a1 = page.addFreetextAnnot(r1, t, color=red) - a2 = page.addFreetextAnnot(r2, t, fontname="Ti", color=blue) - a3 = page.addFreetextAnnot(r3, t, fontname="Co", color=blue, rotate=90) + a1 = page.add_freetext_annot(r1, t, color=red) + a2 = page.add_freetext_annot(r2, t, fontname="Ti", color=blue) + a3 = page.add_freetext_annot(r3, t, fontname="Co", color=blue, rotate=90) a3.set_border(width=0) a3.update(fontsize=8, fill_color=gold) @@ -1416,7 +1421,7 @@ How to :index:`Embed or Attach Files ` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ PDF supports incorporating arbitrary data. This can be done in one of two ways: "embedding" or "attaching". PyMuPDF supports both options. -1. Attached Files: data are **attached to a page** by way of a *FileAttachment* annotation with this statement: *annot = page.addFileAnnot(pos, ...)*, for details see :meth:`Page.addFileAnnot`. The first parameter "pos" is the :ref:`Point`, where a "PushPin" icon should be placed on the page. +1. Attached Files: data are **attached to a page** by way of a *FileAttachment* annotation with this statement: *annot = page.add_file_annot(pos, ...)*, for details see :meth:`Page.add_file_annot`. The first parameter "pos" is the :ref:`Point`, where a "PushPin" icon should be placed on the page. 2. Embedded Files: data are embedded on the **document level** via method :meth:`Document.embfile_add`. diff --git a/docs/functions.rst b/docs/functions.rst index c1493452e..24904d23a 100644 --- a/docs/functions.rst +++ b/docs/functions.rst @@ -546,13 +546,13 @@ Yet others are handy, general-purpose utilities. 102 >>> imgout.close() - .. note:: There is a functional overlap with *pix = fitz.Pixmap(doc, xref)*, followed by a *pix.getPNGData()*. Main differences are that extract_image, **(1)** does not always deliver PNG image formats, **(2)** is **very** much faster with non-PNG images, **(3)** usually results in much less disk storage for extracted images, **(4)** returns *None* in error cases (generates no exception). Look at the following example images within the same PDF. + .. note:: There is a functional overlap with *pix = fitz.Pixmap(doc, xref)*, followed by a *pix.tobytes()*. Main differences are that extract_image, **(1)** does not always deliver PNG image formats, **(2)** is **very** much faster with non-PNG images, **(3)** usually results in much less disk storage for extracted images, **(4)** returns *None* in error cases (generates no exception). Look at the following example images within the same PDF. * xref 1268 is a PNG -- Comparable execution time and identical output:: - In [23]: %timeit pix = fitz.Pixmap(doc, 1268);pix.getPNGData() + In [23]: %timeit pix = fitz.Pixmap(doc, 1268);pix.tobytes() 10.8 ms ± 52.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) - In [24]: len(pix.getPNGData()) + In [24]: len(pix.tobytes()) Out[24]: 21462 In [25]: %timeit img = doc.extract_image(1268) @@ -562,9 +562,9 @@ Yet others are handy, general-purpose utilities. * xref 1186 is a JPEG -- :meth:`Document.extract_image` is **many times faster** and produces a **much smaller** output (2.48 MB vs. 0.35 MB):: - In [27]: %timeit pix = fitz.Pixmap(doc, 1186);pix.getPNGData() + In [27]: %timeit pix = fitz.Pixmap(doc, 1186);pix.tobytes() 341 ms ± 2.86 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) - In [28]: len(pix.getPNGData()) + In [28]: len(pix.tobytes()) Out[28]: 2599433 In [29]: %timeit img = doc.extract_image(1186) diff --git a/docs/make-bold.py b/docs/make-bold.py index c809d0747..330fe8bdd 100644 --- a/docs/make-bold.py +++ b/docs/make-bold.py @@ -71,6 +71,5 @@ widget.field_value = "Off" # arbitrary value widget.fill_color = (0, 0, 1) # make button visible -annot = page.addWidget(widget) # add the widget to the page +annot = page.add_widget(widget) # add the widget to the page doc.save(o_fn) # output the file - diff --git a/docs/multiprocess-gui.py b/docs/multiprocess-gui.py index 403bc0c4f..762efbc63 100644 --- a/docs/multiprocess-gui.py +++ b/docs/multiprocess-gui.py @@ -46,19 +46,19 @@ def initUI(self): hbox = QtWidgets.QHBoxLayout() self.btnOpen = QtWidgets.QPushButton("OpenDocument", self) self.btnOpen.clicked.connect(self.openDoc) - hbox.addWidget(self.btnOpen) + hbox.add_widget(self.btnOpen) self.btnPlay = QtWidgets.QPushButton("PlayDocument", self) self.btnPlay.clicked.connect(self.playDoc) - hbox.addWidget(self.btnPlay) + hbox.add_widget(self.btnPlay) self.btnStop = QtWidgets.QPushButton("Stop", self) self.btnStop.clicked.connect(self.stopPlay) - hbox.addWidget(self.btnStop) + hbox.add_widget(self.btnStop) self.label = QtWidgets.QLabel("0/0", self) self.label.setFont(QtGui.QFont("Verdana", 20)) - hbox.addWidget(self.label) + hbox.add_widget(self.label) vbox.addLayout(hbox) @@ -67,7 +67,7 @@ def initUI(self): QtWidgets.QSizePolicy.Preferred, QtWidgets.QSizePolicy.Expanding ) self.labelImg.setSizePolicy(sizePolicy) - vbox.addWidget(self.labelImg) + vbox.add_widget(self.labelImg) self.setGeometry(100, 100, 400, 600) self.setWindowTitle("PyMuPDF Document Player") diff --git a/docs/multiprocess-render.py b/docs/multiprocess-render.py index c8040bfc9..39ed84a6c 100644 --- a/docs/multiprocess-render.py +++ b/docs/multiprocess-render.py @@ -57,7 +57,7 @@ def render_page(vector): # page.get_text("rawdict") # use any page-related type of work here, eg pix = page.get_pixmap(alpha=False, matrix=mat) # store away the result somewhere ... - # pix.writePNG("p-%i.png" % i) + # pix.save("p-%i.png" % i) print("Processed page numbers %i through %i" % (seg_from, seg_to - 1)) diff --git a/docs/new-annots.py b/docs/new-annots.py index fec64120e..ba2576f4a 100644 --- a/docs/new-annots.py +++ b/docs/new-annots.py @@ -54,11 +54,11 @@ def print_descr(annot): page.set_rotation(0) -annot = page.addCaretAnnot(r.tl) +annot = page.add_caret_annot(r.tl) print_descr(annot) r = r + displ -annot = page.addFreetextAnnot( +annot = page.add_freetext_annot( r, t1, fontsize=10, @@ -72,7 +72,7 @@ def print_descr(annot): print_descr(annot) r = annot.rect + displ -annot = page.addTextAnnot(r.tl, t1) +annot = page.add_text_annot(r.tl, t1) print_descr(annot) # Adding text marker annotations: @@ -84,30 +84,30 @@ def print_descr(annot): morph=(pos, fitz.Matrix(-5)), # rotate around insertion point ) rl = page.search_for(highlight, quads=True) # need a quad b/o tilted text -annot = page.addHighlightAnnot(rl[0]) +annot = page.add_highlight_annot(rl[0]) print_descr(annot) pos = annot.rect.bl # next insertion point page.insert_text(pos, underline, morph=(pos, fitz.Matrix(-10))) rl = page.search_for(underline, quads=True) -annot = page.addUnderlineAnnot(rl[0]) +annot = page.add_underline_annot(rl[0]) print_descr(annot) pos = annot.rect.bl page.insert_text(pos, strikeout, morph=(pos, fitz.Matrix(-15))) rl = page.search_for(strikeout, quads=True) -annot = page.addStrikeoutAnnot(rl[0]) +annot = page.add_strikeout_annot(rl[0]) print_descr(annot) pos = annot.rect.bl page.insert_text(pos, squiggled, morph=(pos, fitz.Matrix(-20))) rl = page.search_for(squiggled, quads=True) -annot = page.addSquigglyAnnot(rl[0]) +annot = page.add_squiggly_annot(rl[0]) print_descr(annot) pos = annot.rect.bl r = fitz.Rect(pos, pos.x + 75, pos.y + 35) + (0, 20, 0, 20) -annot = page.addPolylineAnnot([r.bl, r.tr, r.br, r.tl]) # 'Polyline' +annot = page.add_polyline_annot([r.bl, r.tr, r.br, r.tl]) # 'Polyline' annot.set_border(width=0.3, dashes=[2]) annot.set_colors(stroke=blue, fill=green) annot.set_line_ends(fitz.PDF_ANNOT_LE_CLOSED_ARROW, fitz.PDF_ANNOT_LE_R_CLOSED_ARROW) @@ -115,7 +115,7 @@ def print_descr(annot): print_descr(annot) r += displ -annot = page.addPolygonAnnot([r.bl, r.tr, r.br, r.tl]) # 'Polygon' +annot = page.add_polygon_annot([r.bl, r.tr, r.br, r.tl]) # 'Polygon' annot.set_border(width=0.3, dashes=[2]) annot.set_colors(stroke=blue, fill=gold) annot.set_line_ends(fitz.PDF_ANNOT_LE_DIAMOND, fitz.PDF_ANNOT_LE_CIRCLE) @@ -123,7 +123,7 @@ def print_descr(annot): print_descr(annot) r += displ -annot = page.addLineAnnot(r.tr, r.bl) # 'Line' +annot = page.add_line_annot(r.tr, r.bl) # 'Line' annot.set_border(width=0.3, dashes=[2]) annot.set_colors(stroke=blue, fill=gold) annot.set_line_ends(fitz.PDF_ANNOT_LE_DIAMOND, fitz.PDF_ANNOT_LE_CIRCLE) @@ -131,21 +131,21 @@ def print_descr(annot): print_descr(annot) r += displ -annot = page.addRectAnnot(r) # 'Square' +annot = page.add_rect_annot(r) # 'Square' annot.set_border(width=1, dashes=[1, 2]) annot.set_colors(stroke=blue, fill=gold) annot.update(opacity=0.5) print_descr(annot) r += displ -annot = page.addCircleAnnot(r) # 'Circle' +annot = page.add_circle_annot(r) # 'Circle' annot.set_border(width=0.3, dashes=[2]) annot.set_colors(stroke=blue, fill=gold) annot.update() print_descr(annot) r += displ -annot = page.addFileAnnot( +annot = page.add_file_annot( r.tl, b"just anything for testing", "testdata.txt" # 'FileAttachment' ) print_descr(annot) # annot.rect @@ -163,7 +163,7 @@ def print_descr(annot): color=blue, align=fitz.TEXT_ALIGN_CENTER, ) -annot = page.addRedactAnnot(r) +annot = page.add_redact_annot(r) print_descr(annot) doc.save(__file__.replace(".py", "-%i.pdf" % page.rotation), deflate=True) diff --git a/docs/page.rst b/docs/page.rst index 043fb3828..469afa2d1 100644 --- a/docs/page.rst +++ b/docs/page.rst @@ -35,23 +35,23 @@ In a nutshell, this is what you can do with PyMuPDF: ================================== ======================================================= **Method / Attribute** **Short Description** ================================== ======================================================= -:meth:`Page.addCaretAnnot` PDF only: add a caret annotation -:meth:`Page.addCircleAnnot` PDF only: add a circle annotation -:meth:`Page.addFileAnnot` PDF only: add a file attachment annotation -:meth:`Page.addFreetextAnnot` PDF only: add a text annotation -:meth:`Page.addHighlightAnnot` PDF only: add a "highlight" annotation -:meth:`Page.addInkAnnot` PDF only: add an ink annotation -:meth:`Page.addLineAnnot` PDF only: add a line annotation -:meth:`Page.addPolygonAnnot` PDF only: add a polygon annotation -:meth:`Page.addPolylineAnnot` PDF only: add a multi-line annotation -:meth:`Page.addRectAnnot` PDF only: add a rectangle annotation -:meth:`Page.addRedactAnnot` PDF only: add a redaction annotation -:meth:`Page.addSquigglyAnnot` PDF only: add a "squiggly" annotation -:meth:`Page.addStampAnnot` PDF only: add a "rubber stamp" annotation -:meth:`Page.addStrikeoutAnnot` PDF only: add a "strike-out" annotation -:meth:`Page.addTextAnnot` PDF only: add a comment -:meth:`Page.addUnderlineAnnot` PDF only: add an "underline" annotation -:meth:`Page.addWidget` PDF only: add a PDF Form field +:meth:`Page.add_caret_annot` PDF only: add a caret annotation +:meth:`Page.add_circle_annot` PDF only: add a circle annotation +:meth:`Page.add_file_annot` PDF only: add a file attachment annotation +:meth:`Page.add_freetext_annot` PDF only: add a text annotation +:meth:`Page.add_highlight_annot` PDF only: add a "highlight" annotation +:meth:`Page.add_ink_annot` PDF only: add an ink annotation +:meth:`Page.add_line_annot` PDF only: add a line annotation +:meth:`Page.add_polygon_annot` PDF only: add a polygon annotation +:meth:`Page.add_polyline_annot` PDF only: add a multi-line annotation +:meth:`Page.add_rect_annot` PDF only: add a rectangle annotation +:meth:`Page.add_redact_annot` PDF only: add a redaction annotation +:meth:`Page.add_squiggly_annot` PDF only: add a "squiggly" annotation +:meth:`Page.add_stamp_annot` PDF only: add a "rubber stamp" annotation +:meth:`Page.add_strikeout_annot` PDF only: add a "strike-out" annotation +:meth:`Page.add_text_annot` PDF only: add a comment +:meth:`Page.add_underline_annot` PDF only: add an "underline" annotation +:meth:`Page.add_widget` PDF only: add a PDF Form field :meth:`Page.annot_names` PDF only: a list of annotation and widget names :meth:`Page.annots` return a generator over the annots on the page :meth:`Page.apply_redactions` PDF olny: process the redactions of the page @@ -128,7 +128,7 @@ In a nutshell, this is what you can do with PyMuPDF: :rtype: :ref:`Rect` - .. method:: addCaretAnnot(point) + .. method:: add_caret_annot(point) *(New in version 1.16.0)* @@ -142,7 +142,7 @@ In a nutshell, this is what you can do with PyMuPDF: .. image:: images/img-caret-annot.* :scale: 70 - .. method:: addTextAnnot(point, text, icon="Note") + .. method:: add_text_annot(point, text, icon="Note") PDF only: Add a comment icon ("sticky note") with accompanying text. Only the icon is visible, the accompanying text is hidden and can be visualized by many PDF viewers by hovering the mouse over the symbol. @@ -155,14 +155,14 @@ In a nutshell, this is what you can do with PyMuPDF: :returns: the created annotation. .. index:: - pair: color; addFreetextAnnot - pair: fontname; addFreetextAnnot - pair: fontsize; addFreetextAnnot - pair: rect; addFreetextAnnot - pair: rotate; addFreetextAnnot - pair: align; addFreetextAnnot + pair: color; add_freetext_annot + pair: fontname; add_freetext_annot + pair: fontsize; add_freetext_annot + pair: rect; add_freetext_annot + pair: rotate; add_freetext_annot + pair: align; add_freetext_annot - .. method:: addFreetextAnnot(rect, text, fontsize=12, fontname="helv", text_color=0, fill_color=1, rotate=0, align=TEXT_ALIGN_LEFT) + .. method:: add_freetext_annot(rect, text, fontsize=12, fontname="helv", text_color=0, fill_color=1, rotate=0, align=TEXT_ALIGN_LEFT) PDF only: Add text in a given rectangle. @@ -182,7 +182,7 @@ In a nutshell, this is what you can do with PyMuPDF: :rtype: :ref:`Annot` :returns: the created annotation. Color properties **can only be changed** using special parameters of :meth:`Annot.update`. There, you can also set a border color different from the text color. - .. method:: addFileAnnot(pos, buffer, filename, ufilename=None, desc=None, icon="PushPin") + .. method:: add_file_annot(pos, buffer, filename, ufilename=None, desc=None, icon="PushPin") PDF only: Add a file attachment annotation with a "PushPin" icon at the specified location. @@ -200,7 +200,7 @@ In a nutshell, this is what you can do with PyMuPDF: :rtype: :ref:`Annot` :returns: the created annotation. Use methods of :ref:`Annot` to make any changes. - .. method:: addInkAnnot(list) + .. method:: add_ink_annot(list) PDF only: Add a "freehand" scribble annotation. @@ -209,7 +209,7 @@ In a nutshell, this is what you can do with PyMuPDF: :rtype: :ref:`Annot` :returns: the created annotation in default appearance (black line of width 1). Use annotation methods with a subsequent :meth:`Annot.update` to modify. - .. method:: addLineAnnot(p1, p2) + .. method:: add_line_annot(p1, p2) PDF only: Add a line annotation. @@ -220,9 +220,9 @@ In a nutshell, this is what you can do with PyMuPDF: :rtype: :ref:`Annot` :returns: the created annotation. It is drawn with line color black and line width 1. The **rectangle** is automatically created to contain both points, each one surrounded by a circle of radius 3 * line width to make room for any line end symbols. - .. method:: addRectAnnot(rect) + .. method:: add_rect_annot(rect) - .. method:: addCircleAnnot(rect) + .. method:: add_circle_annot(rect) PDF only: Add a rectangle, resp. circle annotation. @@ -231,7 +231,7 @@ In a nutshell, this is what you can do with PyMuPDF: :rtype: :ref:`Annot` :returns: the created annotation. It is drawn with line color red, no fill color and line width 1. - .. method:: addRedactAnnot(quad, text=None, fontname=None, fontsize=11, align=TEXT_ALIGN_LEFT, fill=(1, 1, 1), text_color=(0, 0, 0), cross_out=True) + .. method:: add_redact_annot(quad, text=None, fontname=None, fontsize=11, align=TEXT_ALIGN_LEFT, fill=(1, 1, 1), text_color=(0, 0, 0), cross_out=True) PDF only: *(new in version 1.16.11)* Add a redaction annotation. A redaction annotation identifies content to be removed from the document. Adding such an annotation is the first of two steps. It makes visible what will be removed in the subsequent step, :meth:`Page.apply_redactions`. @@ -252,7 +252,7 @@ In a nutshell, this is what you can do with PyMuPDF: fontfile="...", # desired font file render_mode=3, # makes the text invisible ) - page.addRedactAnnot(..., fontname="newname") + page.add_redact_annot(..., fontname="newname") :arg float fontsize: *(New in v1.16.12)* the fontsize to use for the replacing text. If the text is too large to fit, several insertion attempts will be made, gradually reducing the fontsize to no less than 4. If then the text will still not fit, no text insertion will take place at all. @@ -269,9 +269,9 @@ In a nutshell, this is what you can do with PyMuPDF: .. image:: images/img-redact.* - .. method:: addPolylineAnnot(points) + .. method:: add_polyline_annot(points) - .. method:: addPolygonAnnot(points) + .. method:: add_polygon_annot(points) PDF only: Add an annotation consisting of lines which connect the given points. A **Polygon's** first and last points are automatically connected, which does not happen for a **PolyLine**. The **rectangle** is automatically created as the smallest rectangle containing the points, each one surrounded by a circle of radius 3 (= 3 * line width). The following shows a 'PolyLine' that has been modified with colors and line ends. @@ -283,13 +283,13 @@ In a nutshell, this is what you can do with PyMuPDF: .. image:: images/img-polyline.* :scale: 70 - .. method:: addUnderlineAnnot(quads=None, start=None, stop=None, clip=None) + .. method:: add_underline_annot(quads=None, start=None, stop=None, clip=None) - .. method:: addStrikeoutAnnot(quads=None, start=None, stop=None, clip=None) + .. method:: add_strikeout_annot(quads=None, start=None, stop=None, clip=None) - .. method:: addSquigglyAnnot(quads=None, start=None, stop=None, clip=None) + .. method:: add_squiggly_annot(quads=None, start=None, stop=None, clip=None) - .. method:: addHighlightAnnot(quads=None, start=None, stop=None, clip=None) + .. method:: add_highlight_annot(quads=None, start=None, stop=None, clip=None) PDF only: These annotations are normally used for **marking text** which has previously been somehow located (for example via :meth:`Page.search_for`). But this is not required: you are free to "mark" just anything. @@ -303,7 +303,7 @@ In a nutshell, this is what you can do with PyMuPDF: >>> # always prefer quads=True in text searching! >>> quads = page.search_for("pymupdf", quads=True) - >>> page.addHighlightAnnot(quads) + >>> page.add_highlight_annot(quads) .. note:: Obviously, text marker annotations need to know what is the top, the bottom, the left, and the right side of the area(s) to be marked. If the arguments are quads, this information is given by the sequence of the quad points. In contrast, a rectangle delivers much less information -- this is illustrated by the fact, that 4! = 24 different quads can be constructed with the four corners of a reactangle. @@ -323,7 +323,7 @@ In a nutshell, this is what you can do with PyMuPDF: .. image:: images/img-markers.* :scale: 100 - .. method:: addStampAnnot(rect, stamp=0) + .. method:: add_stamp_annot(rect, stamp=0) PDF only: Add a "rubber stamp" like annotation to e.g. indicate the document's intended use ("DRAFT", "CONFIDENTIAL", etc.). @@ -342,7 +342,7 @@ In a nutshell, this is what you can do with PyMuPDF: .. image :: images/img-stampannot.* :scale: 80 - .. method:: addWidget(widget) + .. method:: add_widget(widget) PDF only: Add a PDF Form field ("widget") to a page. This also **turns the PDF into a Form PDF**. Because of the large amount of different options available for widgets, we have developed a new class :ref:`Widget`, which contains the possible PDF field attributes. It must be used for both, form field creation and updates. @@ -847,16 +847,18 @@ In a nutshell, this is what you can do with PyMuPDF: pair: pixmap; insert_image pair: rotate; insert_image pair: stream; insert_image + pair: alpha; insert_image pair: mask; insert_image + pair: alpha; insert_image pair: oc; insert_image pair: xref; insert_image - .. method:: insert_image(rect, filename=None, pixmap=None, stream=None, mask=None, rotate=0, oc=0, xref=0, keep_proportion=True, overlay=True) + .. method:: insert_image(rect, filename=None, pixmap=None, stream=None, mask=None, rotate=0, alpha=-1, oc=0, xref=0, keep_proportion=True, overlay=True) - PDF only: Put an image inside the given rectangle. The image can be taken either from an existing image in the PDF, provided as xref number, or otherwise from a pixmap, a file, or a memory area - of these parameters **exactly one** must be specified. + PDF only: Put an image inside the given rectangle. The image may already exist in the PDF or be taken from a pixmap, a file, or a memory area. * Changed in version 1.14.1: By default, the image keeps its aspect ratio. - * Changed in version 1.18.13: Allow providing the image as xref of an existing one. + * Changed in version 1.18.13: Allow providing the image as the xref of an existing one. :arg rect_like rect: where to put the image. Must be finite and not empty. @@ -864,22 +866,24 @@ In a nutshell, this is what you can do with PyMuPDF: *(Changed in version 1.14.13)* The image is now always placed **centered** in the rectangle, i.e. the centers of image and rectangle are equal. - :arg str filename: name of an image file (all formats supported by MuPDF -- see :ref:`ImageFiles`). If the same image is to be inserted multiple times, choose one of the other two options to avoid some overhead. + :arg str filename: name of an image file (all formats supported by MuPDF -- see :ref:`ImageFiles`). - :arg bytes,bytearray,io.BytesIO stream: image in memory (all formats supported by MuPDF -- see :ref:`ImageFiles`). This is the most efficient option. + :arg bytes,bytearray,io.BytesIO stream: image in memory (all formats supported by MuPDF -- see :ref:`ImageFiles`). Changed in version 1.14.13: *io.BytesIO* is now also supported. :arg pixmap: a pixmap containing the image. :type pixmap: :ref:`Pixmap` - :arg bytes,bytearray,io.BytesIO mask: *(new in version v1.18.1)* image in memory -- to be used as image mask for the base image. When specified, the base image must also be provided as an in-memory image (*stream* parameter). + :arg bytes,bytearray,io.BytesIO mask: *(new in version v1.18.1)* image in memory -- to be used as image mask (alpha values) for the base image. When specified, the base image must be provided as a filename or a stream. - :arg int xref: *(New in v1.18.13)* the :data:`xref` of an image already present in the PDF. If given (xref > 0), parameters ``filename``, ``pixmap``, ``stream`` and ``mask`` are ignored. The page will simply receive a reference to the exsting image. + :arg int xref: *(New in v1.18.13)* the :data:`xref` of an image already present in the PDF. If given, parameters ``filename``, ``pixmap``, ``stream``, ``alpha`` and ``mask`` are ignored. The page will simply receive a reference to the exsting image. + + :arg int alpha: *(New in v1.18.13)* if set to 0, the method will assume and not check that the image has no alpha channel. This can speed up execution considerably. Use if image information is available from other sources. Affects insertions from files or streams. :arg int rotate: *(new in version v1.14.11)* rotate the image. Must be an integer multiple of 90 degrees. If you need a rotation by an arbitrary angle, consider converting the image to a PDF (:meth:`Document.convert_to_pdf`) first and then use :meth:`Page.show_pdf_page` instead. - :arg int oc: *(new in v1.18.3)* (:data:`xref`) make image visibility dependent on this OCG (optional content group). This only happens, when inserting the image for the first time -- and consequently is ignored if xref is specified. This property is stored with the generated PDF image object and therefore controls the image's visibility throughout the PDF. + :arg int oc: *(new in v1.18.3)* (:data:`xref`) make image visibility dependent on this :data:`OCG` or :data:`OCMD`. Ignored after the first of multiple insertions. The property is stored with the generated PDF image object and therefore controls the image's visibility throughout the PDF. :arg bool keep_proportion: *(new in version v1.14.11)* maintain the aspect ratio of the image. For a description of *overlay* see :ref:`CommonParms`. @@ -887,14 +891,14 @@ In a nutshell, this is what you can do with PyMuPDF: *Changed in v1.18.13:* Return xref of stored image. :rtype: int - :returns: The xref of the embedded image. This can be used as the ``xref`` argument for very significant performance boosts, if the image must be inserted again. + :returns: The xref of the embedded image. This can be used as the ``xref`` argument for very significant performance boosts, if the image is inserted again. This example puts the same image on every page of a document:: >>> doc = fitz.open(...) >>> rect = fitz.Rect(0, 0, 50, 50) # put thumbnail in upper left corner >>> img = open("some.jpg", "rb").read() # an image file - >>> img_xref = 0 # first execution embeds image + >>> img_xref = 0 # first execution embeds the image >>> for page in doc: img_xref = page.insert_image(rect, stream=img, xref=img_xref, 2nd time reuses existing image @@ -905,9 +909,9 @@ In a nutshell, this is what you can do with PyMuPDF: 1. The method detects multiple insertions of the same image (like in above example) and will store its data only on the first execution. This is even true, if using the default ``xref=0``. - 2. The method cannot detect if the same image had already been part of the file before opening it. This type of situation can only be resolved by saving with ``garbage=4``. + 2. The method cannot detect if the same image had already been part of the file before opening it. - 3. You can use this method to provide a background or foreground image for the page, like a copyright or a watermark. Please remember, that watermarks require a transparent image ... + 3. You can use this method to provide a background or foreground image for the page, like a copyright or a watermark. Please remember, that watermarks require a transparent image if put in foreground ... 4. The image may be inserted uncompressed, e.g. if a *Pixmap* is used or if the image has an alpha channel. Therefore, consider using *deflate=True* when saving the file. In addition, there exist effective ways to control the image size -- even if transparency comes into play. Have a look at `this `_ section of the documentation. diff --git a/docs/pixmap.rst b/docs/pixmap.rst index 581ddf69d..3fb8dd643 100644 --- a/docs/pixmap.rst +++ b/docs/pixmap.rst @@ -23,24 +23,22 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". ============================= =================================================== **Method / Attribute** **Short Description** ============================= =================================================== -:meth:`Pixmap.clearWith` clear parts of a pixmap -:meth:`Pixmap.copyPixmap` copy parts of another pixmap -:meth:`Pixmap.gammaWith` apply a gamma factor to the pixmap -:meth:`Pixmap.getImageData` return a memory area in a variety of formats -:meth:`Pixmap.getPNGData` return a PNG as a memory area -:meth:`Pixmap.invertIRect` invert the pixels of a given area -:meth:`Pixmap.pillowWrite` save as image using pillow (experimental) -:meth:`Pixmap.pillowData` write image stream using pillow (experimental) +:meth:`Pixmap.clear_with` clear parts of a pixmap +:meth:`Pixmap.copy` copy parts of another pixmap +:meth:`Pixmap.gamma_with` apply a gamma factor to the pixmap +:meth:`Pixmap.tobytes` return a memory area in a variety of formats +:meth:`Pixmap.invert_irect` invert the pixels of a given area +:meth:`Pixmap.pil_save` save as image using pillow (experimental) +:meth:`Pixmap.pil_tobytes` write image stream using pillow (experimental) :meth:`Pixmap.pixel` return the value of a pixel -:meth:`Pixmap.setAlpha` set alpha values -:meth:`Pixmap.setPixel` set the color of a pixel -:meth:`Pixmap.setRect` set the color of a rectangle -:meth:`Pixmap.setResolution` set the image resolution -:meth:`Pixmap.setOrigin` set pixmap x,y values +:meth:`Pixmap.set_alpha` set alpha values +:meth:`Pixmap.set_pixel` set the color of a pixel +:meth:`Pixmap.set_rect` set the color of a rectangle +:meth:`Pixmap.set_dpi` set the image resolution +:meth:`Pixmap.set_origin` set pixmap x,y values :meth:`Pixmap.shrink` reduce size keeping proportions -:meth:`Pixmap.tintWith` tint a pixmap with a color -:meth:`Pixmap.writeImage` save a pixmap in a variety of formats -:meth:`Pixmap.writePNG` save a pixmap as a PNG file +:meth:`Pixmap.tint_with` tint a pixmap with a color +:meth:`Pixmap.save` save a pixmap in a variety of formats :attr:`Pixmap.alpha` transparency indicator :attr:`Pixmap.colorspace` pixmap's :ref:`Colorspace` :attr:`Pixmap.digest` MD5 hashcode of the pixmap @@ -64,7 +62,7 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". .. method:: __init__(self, colorspace, irect, alpha) - **New empty pixmap:** Create an empty pixmap of size and origin given by the rectangle. So, *irect.top_left* designates the top left corner of the pixmap, and its width and height are *irect.width* resp. *irect.height*. Note that the image area is **not initialized** and will contain crap data -- use eg. :meth:`clearWith` or :meth:`setRect` to be sure. + **New empty pixmap:** Create an empty pixmap of size and origin given by the rectangle. So, *irect.top_left* designates the top left corner of the pixmap, and its width and height are *irect.width* resp. *irect.height*. Note that the image area is **not initialized** and will contain crap data -- use eg. :meth:`clear_with` or :meth:`set_rect` to be sure. :arg colorspace: colorspace. :type colorspace: :ref:`Colorspace` @@ -162,7 +160,7 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :arg int xref: the :data:`xref` of an image object. For example, you can make a list of images used on a particular page with :meth:`Document.get_page_images`, which also shows the :data:`xref` numbers of each image. - .. method:: clearWith([value [, irect]]) + .. method:: clear_with([value [, irect]]) Initialize the samples area. @@ -170,7 +168,7 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :arg irect_like irect: the area to be cleared. Omit to clear the whole pixmap. Can only be specified, if *value* is also specified. - .. method:: tintWith(red, green, blue) + .. method:: tint_with(red, green, blue) Colorize (tint) a pixmap with a color provided as an integer triple (red, green, blue). Only colorspaces :data:`CS_GRAY` and :data:`CS_RGB` are supported, others are ignored with a warning. @@ -182,7 +180,7 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :arg int blue: *blue* component. - .. method:: gammaWith(gamma) + .. method:: gamma_with(gamma) Apply a gamma factor to a pixmap, i.e. lighten or darken it. Pixmaps with colorspace *None* are ignored with a warning. @@ -206,7 +204,7 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :rtype: list :returns: a list of color values and, potentially the alpha value. Its length and content depend on the pixmap's colorspace and the presence of an alpha. For RGBA pixmaps the result would e.g. be *[r, g, b, a]*. All items are integers in *range(256)*. - .. method:: setPixel(x, y, color) + .. method:: set_pixel(x, y, color) *New in version 1.14.7:* Set the color of the pixel at location (x, y) (column, line). @@ -214,7 +212,7 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :arg int y: the line number of the pixel. Must be in *range(pix.height)*. :arg sequence color: the desired color given as a sequence of integers in *range(256)*. The length of the sequence must equal :attr:`Pixmap.n`, which includes any alpha byte. - .. method:: setRect(irect, color) + .. method:: set_rect(irect, color) *New in version 1.14.8:* Set the pixels of a rectangle to a color. @@ -226,10 +224,10 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". .. note:: - 1. This method is equivalent to :meth:`Pixmap.setPixel` executed for each pixel in the rectangle, but is obviously **very much faster** if many pixels are involved. - 2. This method can be used similar to :meth:`Pixmap.clearWith` to initialize a pixmap with a certain color like this: *pix.setRect(pix.irect, (255, 255, 0))* (RGB example, colors the complete pixmap with yellow). + 1. This method is equivalent to :meth:`Pixmap.set_pixel` executed for each pixel in the rectangle, but is obviously **very much faster** if many pixels are involved. + 2. This method can be used similar to :meth:`Pixmap.clear_with` to initialize a pixmap with a certain color like this: *pix.set_rect(pix.irect, (255, 255, 0))* (RGB example, colors the complete pixmap with yellow). - .. method:: setOrigin(x, y) + .. method:: set_origin(x, y) *(New in v1.17.7)* Set the x and y values. @@ -237,7 +235,7 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :arg int y: y coordinate - .. method:: setResolution(xres, yres) + .. method:: set_dpi(xres, yres) *(New in v1.16.17)* Set the resolution (dpi) in x and y direction. @@ -247,7 +245,7 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :arg int yres: resolution in y direction. - .. method:: setAlpha(alphavalues, premultiply=1, opaque=None) + .. method:: set_alpha(alphavalues, premultiply=1, opaque=None) *(Changed in v 1.18.13)* @@ -258,26 +256,26 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :arg list,tuple opaque: make this color fully invisible (transparent). A sequence of integers 0 <= i <= 255 with a length of :attr:`Pixmap.n`. Default is *None*. E.g. in the RGB case a typical choice would be ``opaque=(255, 255, 255)`` for white. - .. method:: invertIRect([irect]) + .. method:: invert_irect([irect]) Invert the color of all pixels in :ref:`IRect` *irect*. Will have no effect if colorspace is *None*. :arg irect_like irect: The area to be inverted. Omit to invert everything. - .. method:: copyPixmap(source, irect) + .. method:: copy(source, irect) Copy the *irect* part of the *source* pixmap into the corresponding area of this one. The two pixmaps may have different dimensions and can each have :data:`CS_GRAY` or :data:`CS_RGB` colorspaces, but they currently **must** have the same alpha property [#f2]_. The copy mechanism automatically adjusts discrepancies between source and target like so: If copying from :data:`CS_GRAY` to :data:`CS_RGB`, the source gray-shade value will be put into each of the three rgb component bytes. If the other way round, *(r + g + b) / 3* will be taken as the gray-shade value of the target. - Between *irect* and the target pixmap's rectangle, an "intersection" is calculated at first. This takes into account the rectangle coordinates and the current attribute values ``source.x`` and ``source.y`` (which you are free to modify for this purpose via :meth:`Pixmap.setOrigin`). Then the corresponding data of this intersection are copied. If the intersection is empty, nothing will happen. + Between *irect* and the target pixmap's rectangle, an "intersection" is calculated at first. This takes into account the rectangle coordinates and the current attribute values ``source.x`` and ``source.y`` (which you are free to modify for this purpose via :meth:`Pixmap.set_origin`). Then the corresponding data of this intersection are copied. If the intersection is empty, nothing will happen. :arg source: source pixmap. :type source: :ref:`Pixmap` :arg irect_like irect: The area to be copied. - .. method:: writeImage(filename, output=None) + .. method:: save(filename, output=None) Save pixmap as an image file. Depending on the output chosen, only some or all colorspaces are supported and different file extensions can be chosen. Please see the table below. Since MuPDF v1.10a the *savealpha* option is no longer supported and will be silently ignored. @@ -285,27 +283,19 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". :arg str output: The requested image format. The default is the filename's extension. If not recognized, *png* is assumed. For other possible values see :ref:`PixmapOutput`. - .. method:: writePNG(filename) + .. method:: save(filename) - Equal to *pix.writeImage(filename, "png")*. + Equal to *pix.save(filename, "png")*. - .. method:: getImageData(output="png") + .. method:: tobytes(output="png") - *New in version 1.14.5:* Return the pixmap as a *bytes* memory object of the specified format -- similar to :meth:`writeImage`. + *New in version 1.14.5:* Return the pixmap as a *bytes* memory object of the specified format -- similar to :meth:`save`. - :arg str output: The requested image format. The default is "png" for which this function equals :meth:`getPNGData`. For other possible values see :ref:`PixmapOutput`. + :arg str output: The requested image format. The default is "png" for which this function equals :meth:`tobytes`. For other possible values see :ref:`PixmapOutput`. :rtype: bytes - .. method:: getPNGdata() - - .. method:: getPNGData() - - Equal to *pix.getImageData("png")*. - - :rtype: bytes - - .. method:: pillowWrite(*args, **kwargs) + .. method:: pil_save(*args, **kwargs) *(New in v1.17.3)* @@ -315,15 +305,15 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". * Storing EXIF information. * If you do not provide dpi information, the values *xres*, *yres* stored with the pixmap are automatically used. - A simple example: ``pix.pillowWrite("some.jpg", optimize=True, dpi=(150, 150))``. For details on other parameters see the Pillow documentation. + A simple example: ``pix.pil_save("some.jpg", optimize=True, dpi=(150, 150))``. For details on other parameters see the Pillow documentation. - .. note:: *(Changed in v1.18.0)* :meth:`Pixmap.writeImage` and :meth:`Pixmap.writePNG` now also set resolution / dpi from *xres* / *yres* automatically, when saving a PNG image. + .. note:: *(Changed in v1.18.0)* :meth:`Pixmap.save` and :meth:`Pixmap.save` now also set resolution / dpi from *xres* / *yres* automatically, when saving a PNG image. - .. method:: pillowData(*args, **kwargs) + .. method:: pil_tobytes(*args, **kwargs) *(New in v1.17.3)* - Return an image as a bytes object in the specified format using Pillow. For example ``stream = pix.pillowData(format="JPEG", optimize=True)``. Also see above. For details on other parameters see the Pillow documentation. + Return an image as a bytes object in the specified format using Pillow. For example ``stream = pix.pil_tobytes(format="JPEG", optimize=True)``. Also see above. For details on other parameters see the Pillow documentation. .. attribute:: alpha @@ -395,13 +385,13 @@ Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". .. attribute:: x - X-coordinate of top-left corner in pixels. Cannot directly be changed -- use :meth:`Pixmap.setOrigin`. + X-coordinate of top-left corner in pixels. Cannot directly be changed -- use :meth:`Pixmap.set_origin`. :type: int .. attribute:: y - Y-coordinate of top-left corner in pixels. Cannot directly be changed -- use :meth:`Pixmap.setOrigin`. + Y-coordinate of top-left corner in pixels. Cannot directly be changed -- use :meth:`Pixmap.set_origin`. :type: int @@ -445,7 +435,7 @@ The following file types are supported as **input** to construct pixmaps: **BMP, Supported Output Image Formats --------------------------------------------------------------------------- -A number of image **output** formats are supported. You have the option to either write an image directly to a file (:meth:`Pixmap.writeImage`), or to generate a bytes object (:meth:`Pixmap.getImageData`). Both methods accept a 3-letter string identifying the desired format (**Format** column below). Please note that not all combinations of pixmap colorspace, transparency support (alpha) and image format are possible. +A number of image **output** formats are supported. You have the option to either write an image directly to a file (:meth:`Pixmap.save`), or to generate a bytes object (:meth:`Pixmap.tobytes`). Both methods accept a 3-letter string identifying the desired format (**Format** column below). Please note that not all combinations of pixmap colorspace, transparency support (alpha) and image format are possible. ========== =============== ========= ============== =========================== **Format** **Colorspaces** **alpha** **Extensions** **Description** @@ -464,7 +454,7 @@ psd gray, rgb, cmyk yes .psd Adobe Photoshop Document * Not all image file types are supported (or at least common) on all OS platforms. E.g. PAM and the Portable Anymap formats are rare or even unknown on Windows. * Especially pertaining to CMYK colorspaces, you can always convert a CMYK pixmap to an RGB pixmap with *rgb_pix = fitz.Pixmap(fitz.csRGB, cmyk_pix)* and then save that in the desired format. * As can be seen, MuPDF's image support range is different for input and output. Among those supported both ways, PNG is probably the most popular. We recommend using Pillow whenever you face a support gap. - * We also recommend using "ppm" formats as input to tkinter's *PhotoImage* method like this: *tkimg = tkinter.PhotoImage(data=pix.getImageData("ppm"))* (also see the tutorial). This is **very** fast (**60 times** faster than PNG) and will work under Python 2 or 3. + * We also recommend using "ppm" formats as input to tkinter's *PhotoImage* method like this: *tkimg = tkinter.PhotoImage(data=pix.tobytes("ppm"))* (also see the tutorial). This is **very** fast (**60 times** faster than PNG) and will work under Python 2 or 3. diff --git a/docs/quad.rst b/docs/quad.rst index cae74859f..f8c125b03 100644 --- a/docs/quad.rst +++ b/docs/quad.rst @@ -6,7 +6,7 @@ Quad Represents a four-sided mathematical shape (also called "quadrilateral" or "tetragon") in the plane, defined as a sequence of four :ref:`Point` objects ul, ur, ll, lr (conveniently called upper left, upper right, lower left, lower right). -Quads can **be obtained** as results of text search methods (:meth:`Page.search_for`), and they **are used** to define text marker annotations (see e.g. :meth:`Page.addSquigglyAnnot` and friends), and in several draw methods (like :meth:`Page.draw_quad` / :meth:`Shape.draw_quad`, :meth:`Page.draw_oval`/ :meth:`Shape.draw_quad`). +Quads can **be obtained** as results of text search methods (:meth:`Page.search_for`), and they **are used** to define text marker annotations (see e.g. :meth:`Page.add_squiggly_annot` and friends), and in several draw methods (like :meth:`Page.draw_quad` / :meth:`Shape.draw_quad`, :meth:`Page.draw_oval`/ :meth:`Shape.draw_quad`). .. note:: diff --git a/docs/textpage.rst b/docs/textpage.rst index 45a2913a5..b2021035b 100644 --- a/docs/textpage.rst +++ b/docs/textpage.rst @@ -162,7 +162,7 @@ In addition, **the full quad information is not lost**: it can be recovered as n * :meth:`recover_line_quad` -- the quad of a line * :meth:`recover_char_quad` -- the quad of a character -As mentioned, using these functions is ever only needed, if the text is **not written horizontally** and you need the quad for text marker annotations (:meth:`Page.addHighlightAnnot` and friends). +As mentioned, using these functions is ever only needed, if the text is **not written horizontally** and you need the quad for text marker annotations (:meth:`Page.add_highlight_annot` and friends). .. image:: images/img-textpage.* diff --git a/docs/tutorial.rst b/docs/tutorial.rst index cdd5a21a3..2cf05d55c 100644 --- a/docs/tutorial.rst +++ b/docs/tutorial.rst @@ -150,7 +150,7 @@ Saving the Page Image in a File ----------------------------------- We can simply store the image in a PNG file:: - pix.writeImage("page-%i.png" % page.number) + pix.save("page-%i.png" % page.number) Displaying the Image in GUIs ------------------------------------------- @@ -180,7 +180,7 @@ The following **avoids using Pillow**:: # remove alpha if present pix1 = fitz.Pixmap(pix, 0) if pix.alpha else pix # PPM does not support transparency - imgdata = pix1.getImageData("ppm") # extremely fast! + imgdata = pix1.tobytes("ppm") # extremely fast! tkimg = tkinter.PhotoImage(data = imgdata) If you are looking for a complete Tkinter script paging through **any supported** document, `here it is! `_ It can also zoom into pages, and it runs under Python 2 or 3. It requires the extremely handy `PySimpleGUI `_ pure Python package. diff --git a/docs/version.rst b/docs/version.rst index 924f8157f..ed8d3df52 100644 --- a/docs/version.rst +++ b/docs/version.rst @@ -1,6 +1,6 @@ Covered Version -------------------- -This documentation covers PyMuPDF v1.18.13 features as of **2021-04-29 09:59:29**. +This documentation covers PyMuPDF v1.18.13 features as of **2021-05-05 06:32:22**. .. note:: The major and minor versions of **PyMuPDF** and **MuPDF** will always be the same. Only the third qualifier (patch level) may deviate from that of MuPDF. \ No newline at end of file diff --git a/fitz/__init__.py b/fitz/__init__.py index 8b34c4020..649e07b6b 100644 --- a/fitz/__init__.py +++ b/fitz/__init__.py @@ -192,6 +192,23 @@ def restore_aliases(): # deprecated Page aliases fitz.Page._isWrapped = fitz.Page.is_wrapped + fitz.Page.addCaretAnnot = fitz.Page.add_caret_annot + fitz.Page.addCircleAnnot = fitz.Page.add_circle_annot + fitz.Page.addFileAnnot = fitz.Page.add_file_annot + fitz.Page.addFreetextAnnot = fitz.Page.add_freetext_annot + fitz.Page.addHighlightAnnot = fitz.Page.add_highlight_annot + fitz.Page.addInkAnnot = fitz.Page.add_ink_annot + fitz.Page.addLineAnnot = fitz.Page.add_line_annot + fitz.Page.addPolygonAnnot = fitz.Page.add_polygon_annot + fitz.Page.addPolylineAnnot = fitz.Page.add_polyline_annot + fitz.Page.addRectAnnot = fitz.Page.add_rect_annot + fitz.Page.addRedactAnnot = fitz.Page.add_redact_annot + fitz.Page.addSquigglyAnnot = fitz.Page.add_squiggly_annot + fitz.Page.addStampAnnot = fitz.Page.add_stamp_annot + fitz.Page.addStrikeoutAnnot = fitz.Page.add_strikeout_annot + fitz.Page.addTextAnnot = fitz.Page.add_text_annot + fitz.Page.addUnderlineAnnot = fitz.Page.add_underline_annot + fitz.Page.addWidget = fitz.Page.add_widget fitz.Page.cleanContents = fitz.Page.clean_contents fitz.Page.CropBox = fitz.Page.cropbox fitz.Page.CropBoxPosition = fitz.Page.cropbox_position diff --git a/fitz/fitz.i b/fitz/fitz.i index 2198d8ea6..a949b245a 100644 --- a/fitz/fitz.i +++ b/fitz/fitz.i @@ -480,14 +480,11 @@ struct Document FITZEXCEPTION(_remove_links_to, !result) - PyObject *_remove_links_to(int first, int last) + PyObject *_remove_links_to(PyObject *numbers) { fz_try(gctx) { - fz_document *doc = (fz_document *) $self; - pdf_document *pdf = pdf_specifics(gctx, doc); - pdf_drop_page_tree(gctx, pdf); - pdf_load_page_tree(gctx, pdf); - remove_dest_range(gctx, pdf, first, last); + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + remove_dest_range(gctx, pdf, numbers); } fz_catch(gctx) { return NULL; @@ -3870,13 +3867,19 @@ if basestate: if item[2] == pno + 1: self.del_toc_item(i) - self._remove_links_to(pno, pno) + self._remove_links_to((pno,)) self._delete_page(pno) self._reset_page_refs() - def delete_pages(self, from_page: int =-1, to_page: int =-1): + def delete_pages(self, *args, **kw): """Delete pages from a PDF. + + Args: + Either keywords 'from_page'/'to_page', or two integers to + specify the first/last page to delete. + Or a list/tuple/range object, which can contain arbitrary + page numbers. """ if not self.is_pdf: raise ValueError("not a PDF") @@ -3884,23 +3887,47 @@ if basestate: raise ValueError("document closed") page_count = self.page_count # page count of document - f = from_page # first page to delete - t = to_page # last page to delete - while f < 0: - f += page_count - while t < 0: - t += page_count - if not f <= t < page_count: + f = t = -1 + if kw: # check if keywords were used + if args != []: # then no positional args are allowed + raise ValueError("cannot mix keyword and positional argument") + f = kw.get("from_page", -1) # first page to delete + t = kw.get("to_page", -1) # last page to delete + while f < 0: + f += page_count + while t < 0: + t += page_count + if not f <= t < page_count: + raise ValueError("bad page number(s)") + numbers = tuple(range(f, t + 1)) + else: + if len(args) > 2 or args == []: + raise ValueError("need 1 or 2 positional arguments") + if len(args) == 2: + f, t = args + if not (type(f) is int and type(t) is int): + raise ValueError("both arguments must be int") + if f > t: + f, t = t, f + numbers = tuple(range(f, t + 1)) + else: + r = args[0] + if type(r) not in (int, range, list, tuple): + raise ValueError("need int or sequence if one argument") + numbers = tuple(r) + + numbers = list(map(int, set(numbers))) # ensure unique integers + numbers.sort() + if numbers[0] < 0 or numbers[-1] >= page_count: raise ValueError("bad page number(s)") - old_toc = self.get_toc() for i, item in enumerate(old_toc): - if f + 1 <= item[2] <= t + 1: + if item[2] - 1 in numbers: # a deleted page number self.del_toc_item(i) - self._remove_links_to(f, t) + self._remove_links_to(numbers) - for i in range(t, f - 1, -1): # delete pages, last to first + for i in reversed(numbers): # delete pages, last to first self._delete_page(i) self._reset_page_refs() @@ -4351,7 +4378,7 @@ struct Page { } //---------------------------------------------------------------- - // page addCaretAnnot + // page add_caret_annot //---------------------------------------------------------------- FITZEXCEPTION(_add_caret_annot, !result) struct Annot * @@ -4815,7 +4842,7 @@ struct Page { """Reflects page de-rotation.""" return Matrix(TOOLS._derotate_matrix(self)) - def addCaretAnnot(self, point: point_like) -> "struct Annot *": + def add_caret_annot(self, point: point_like) -> "struct Annot *": """Add a 'Caret' annotation.""" old_rotation = annot_preprocess(self) try: @@ -4827,7 +4854,7 @@ struct Page { return annot - def addStrikeoutAnnot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *": + def add_strikeout_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *": """Add a 'StrikeOut' annotation.""" if quads is None: q = get_highlight_selection(self, start=start, stop=stop, clip=clip) @@ -4836,7 +4863,7 @@ struct Page { return self._add_text_marker(q, PDF_ANNOT_STRIKE_OUT) - def addUnderlineAnnot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *": + def add_underline_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *": """Add a 'Underline' annotation.""" if quads is None: q = get_highlight_selection(self, start=start, stop=stop, clip=clip) @@ -4845,7 +4872,7 @@ struct Page { return self._add_text_marker(q, PDF_ANNOT_UNDERLINE) - def addSquigglyAnnot(self, quads=None, start=None, + def add_squiggly_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *": """Add a 'Squiggly' annotation.""" if quads is None: @@ -4855,7 +4882,7 @@ struct Page { return self._add_text_marker(q, PDF_ANNOT_SQUIGGLY) - def addHighlightAnnot(self, quads=None, start=None, + def add_highlight_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *": """Add a 'Highlight' annotation.""" if quads is None: @@ -4865,7 +4892,7 @@ struct Page { return self._add_text_marker(q, PDF_ANNOT_HIGHLIGHT) - def addRectAnnot(self, rect: rect_like) -> "struct Annot *": + def add_rect_annot(self, rect: rect_like) -> "struct Annot *": """Add a 'Square' (rectangle) annotation.""" old_rotation = annot_preprocess(self) try: @@ -4877,7 +4904,7 @@ struct Page { return annot - def addCircleAnnot(self, rect: rect_like) -> "struct Annot *": + def add_circle_annot(self, rect: rect_like) -> "struct Annot *": """Add a 'Circle' (ellipse, oval) annotation.""" old_rotation = annot_preprocess(self) try: @@ -4889,7 +4916,7 @@ struct Page { return annot - def addTextAnnot(self, point: point_like, text: str, icon: str ="Note") -> "struct Annot *": + def add_text_annot(self, point: point_like, text: str, icon: str ="Note") -> "struct Annot *": """Add a 'Text' (sticky note) annotation.""" old_rotation = annot_preprocess(self) try: @@ -4901,7 +4928,7 @@ struct Page { return annot - def addLineAnnot(self, p1: point_like, p2: point_like) -> "struct Annot *": + def add_line_annot(self, p1: point_like, p2: point_like) -> "struct Annot *": """Add a 'Line' annotation.""" old_rotation = annot_preprocess(self) try: @@ -4913,7 +4940,7 @@ struct Page { return annot - def addPolylineAnnot(self, points: list) -> "struct Annot *": + def add_polyline_annot(self, points: list) -> "struct Annot *": """Add a 'PolyLine' annotation.""" old_rotation = annot_preprocess(self) try: @@ -4925,7 +4952,7 @@ struct Page { return annot - def addPolygonAnnot(self, points: list) -> "struct Annot *": + def add_polygon_annot(self, points: list) -> "struct Annot *": """Add a 'Polygon' annotation.""" old_rotation = annot_preprocess(self) try: @@ -4937,7 +4964,7 @@ struct Page { return annot - def addStampAnnot(self, rect: rect_like, stamp: int =0) -> "struct Annot *": + def add_stamp_annot(self, rect: rect_like, stamp: int =0) -> "struct Annot *": """Add a ('rubber') 'Stamp' annotation.""" old_rotation = annot_preprocess(self) try: @@ -4949,7 +4976,7 @@ struct Page { return annot - def addInkAnnot(self, handwriting: list) -> "struct Annot *": + def add_ink_annot(self, handwriting: list) -> "struct Annot *": """Add a 'Ink' ('handwriting') annotation. The argument must be a list of lists of point_likes. @@ -4964,7 +4991,7 @@ struct Page { return annot - def addFileAnnot(self, point: point_like, + def add_file_annot(self, point: point_like, buffer: typing.ByteString, filename: str, ufilename: OptStr =None, @@ -4987,7 +5014,7 @@ struct Page { return annot - def addFreetextAnnot(self, rect: rect_like, text: str, fontsize: float =11, + def add_freetext_annot(self, rect: rect_like, text: str, fontsize: float =11, fontname: OptStr =None, text_color: OptSeq =None, fill_color: OptSeq =None, align: int =0, rotate: int =0) -> "struct Annot *": """Add a 'FreeText' annotation.""" @@ -5004,7 +5031,7 @@ struct Page { return annot - def addRedactAnnot(self, quad, text: OptStr =None, fontname: OptStr =None, + def add_redact_annot(self, quad, text: OptStr =None, fontname: OptStr =None, fontsize: float =11, align: int =0, fill: OptSeq =None, text_color: OptSeq =None, cross_out: bool =True) -> "struct Annot *": """Add a 'Redact' annotation.""" @@ -5213,7 +5240,7 @@ def get_oc_items(self) -> list: #--------------------------------------------------------------------- # page addWidget #--------------------------------------------------------------------- - def addWidget(self, widget: Widget) -> "struct Annot *": + def add_widget(self, widget: Widget) -> "struct Annot *": """Add a 'Widget' (form field).""" CheckParent(self) doc = self.parent diff --git a/fitz/helper-select.i b/fitz/helper-select.i index 083491da9..c9b846630 100644 --- a/fitz/helper-select.i +++ b/fitz/helper-select.i @@ -321,23 +321,24 @@ void retainpages(fz_context *ctx, globals *glo, PyObject *liste) pdf_drop_obj(ctx, root); } -PyObject *remove_dest_range(fz_context *ctx, pdf_document *pdf, int first, int last) +void remove_dest_range(fz_context *ctx, pdf_document *pdf, PyObject *numbers) { - int i, pno, pagecount = pdf_count_pages(ctx, pdf); - if (!INRANGE(first, 0, pagecount-1) || - !INRANGE(last, 0, pagecount-1) || - (first > last)) - Py_RETURN_NONE; + int i, j, pno, len, pagecount = pdf_count_pages(ctx, pdf); + PyObject *n1 = NULL; fz_try(ctx) { for (i = 0; i < pagecount; i++) { - if (INRANGE(i, first, last)) continue; + n1 = PyLong_FromLong((long) i); + if (PySequence_Contains(numbers, n1)) { + Py_DECREF(n1); + continue; + } + Py_DECREF(n1); pdf_obj *pageref = pdf_lookup_page_obj(ctx, pdf, i); pdf_obj *annots = pdf_dict_get(ctx, pageref, PDF_NAME(Annots)); pdf_obj *target; if (!annots) continue; - int len = pdf_array_len(ctx, annots); - int j; + len = pdf_array_len(ctx, annots); for (j = len - 1; j >= 0; j -= 1) { pdf_obj *o = pdf_array_get(ctx, annots, j); if (!pdf_name_eq(ctx, pdf_dict_get(ctx, o, PDF_NAME(Subtype)), PDF_NAME(Link))) @@ -360,15 +361,20 @@ PyObject *remove_dest_range(fz_context *ctx, pdf_document *pdf, int first, int l pdf_to_text_string(ctx, dest), NULL, NULL); } - if (INRANGE(pno, first, last)) { + if (pno < 0) { // page lookup did not work + continue; + } + n1 = PyLong_FromLong((long) pno); + if (PySequence_Contains(numbers, n1)) { pdf_array_delete(ctx, annots, j); } + Py_DECREF(n1); } } } fz_catch(ctx) { - return NULL; + fz_rethrow(ctx); } - Py_RETURN_NONE; + return; } %} diff --git a/fitz/utils.py b/fitz/utils.py index ddcb3f105..fb06b4e41 100644 --- a/fitz/utils.py +++ b/fitz/utils.py @@ -230,25 +230,53 @@ def calc_matrix(sr, tr, keep=True, rotate=0): def insert_image(page, rect, **kwargs): + """Insert an image for display in a rectangle. + + Args: + rect: (rect_like) position of image on the page. + alpha: (int, optional) set to 0 if image has no transparency. + filename: (str, Path, file object) image filename. + keep_proportion: (bool) keep width / height ratio (default). + mask: (bytes, optional) image consisting of alpha values to use. + oc: (int) xref of OCG or OCMD to declare as Optional Content. + overlay: (bool) put in foreground (default) or background. + pixmap: (Pixmap) use this as image. + rotate: (int) rotate by 0, 90, 180 or 270 degrees. + stream: (bytes) use this as image. + xref: (int) use this as image. + + 'page' and 'rect' are positional, all other parameters are keywords. + + If 'xref' is given, that image is used. Other input options are ignored. + Else, exactly one of pixmap, stream or filename must be given. + + 'alpha=0' for non-transparent images improves performance significantly. + Affects stream and filename only. + + Optimum transparent insertions are possible by using filename / stream in + conjunction with a 'mask' image of alpha values. + + Returns: + xref (int) of inserted image. Re-use as argument for multiple insertions. + """ CheckParent(page) doc = page.parent if not doc.is_pdf: raise ValueError("not a PDF") valid_keys = { + "alpha", "filename", - "pixmap", - "stream", + "height", + "keep_proportion", "mask", + "oc", + "overlay", + "pixmap", "rotate", + "stream", "width", - "height", - "alpha", - "oc", "xref", - "filename", - "overlay", - "keep_proportion", } s = set(kwargs.keys()).difference(valid_keys) if s != set(): @@ -269,6 +297,16 @@ def insert_image(page, rect, **kwargs): if xref == 0 and (bool(filename) + bool(stream) + bool(pixmap) != 1): raise ValueError("xref=0 needs exactly one of filename, pixmap, stream") + if filename: + if type(filename) is str: + pass + elif hasattr(filename, "absolute"): + filename = str(filename) + elif hasattr(filename, "name"): + filename = filename.name + else: + raise ValueError("bad filename") + if filename and not os.path.exists(filename): raise FileNotFoundError("No such file: '%s'" % filename) elif stream and type(stream) not in (bytes, bytearray, io.BytesIO): diff --git a/fitz/version.i b/fitz/version.i index 0173277e3..5c7f2fdc2 100644 --- a/fitz/version.i +++ b/fitz/version.i @@ -1,6 +1,6 @@ %pythoncode %{ VersionFitz = "1.18.0" VersionBind = "1.18.13" -VersionDate = "2021-05-03 08:51:54" -version = (VersionBind, VersionFitz, "20210503085154") +VersionDate = "2021-05-05 06:32:22" +version = (VersionBind, VersionFitz, "20210505063222") %} \ No newline at end of file diff --git a/tests/resources/image-file1.pdf b/tests/resources/image-file1.pdf new file mode 100644 index 0000000000000000000000000000000000000000..5b896b03c89623eb3cc08ed5b61ac518969edf45 GIT binary patch literal 81019 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 Jw|8?;_#c+}WsCp- literal 0 HcmV?d00001 diff --git a/tests/test_badfonts.py b/tests/test_badfonts.py index b5501f9be..dc55a83d4 100644 --- a/tests/test_badfonts.py +++ b/tests/test_badfonts.py @@ -11,5 +11,5 @@ def test_survive_names(): filename = os.path.join(scriptdir, "resources", "has-bad-fonts.pdf") doc = fitz.open(filename) print("File '%s' uses the following fonts on page 0:" % doc.name) - for f in doc.getPageFontList(0): + for f in doc.get_page_fonts(0): print(f) diff --git a/tests/test_drawings.py b/tests/test_drawings.py index da2ba6b13..acd60ca52 100644 --- a/tests/test_drawings.py +++ b/tests/test_drawings.py @@ -1,6 +1,5 @@ """ -Extract drawings of a PDF page and compare result with content of -a stored text file. +Extract drawings of a PDF page and compare with stored expected result. """ import io import os @@ -14,10 +13,10 @@ def test_drawings(): - symbols_text = open(symbols).read() + symbols_text = open(symbols).read() # expected result doc = fitz.open(filename) page = doc[0] paths = page.get_drawings() - out = io.StringIO() + out = io.StringIO() # pprint output goes here pprint(paths, stream=out) assert symbols_text == out.getvalue() diff --git a/tests/test_geometry.py b/tests/test_geometry.py index 4e739a49f..e4e7940ab 100644 --- a/tests/test_geometry.py +++ b/tests/test_geometry.py @@ -28,9 +28,10 @@ def test_inversion(): alpha = 255 m1 = fitz.Matrix(alpha) m2 = fitz.Matrix(-alpha) - m3 = m1 * m2 + m3 = m1 * m2 # should equal identity matrix assert abs(m3 - fitz.Identity) < fitz.EPSILON m = fitz.Matrix(1, 0, 1, 0, 1, 0) # not invertible! + # inverted matrix must be zero assert ~m == fitz.Matrix() diff --git a/tests/test_imagebbox.py b/tests/test_imagebbox.py new file mode 100644 index 000000000..f69b96ef2 --- /dev/null +++ b/tests/test_imagebbox.py @@ -0,0 +1,33 @@ +""" +Ensure equality of bboxes and transformation matrices computed via +* page.get_image_bbox() +and +* page.get_image_info() +""" +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "image-file1.pdf") +doc = fitz.open(filename) + + +def test_image_bbox(): + page = doc[0] + imglist = page.get_images(True) + bbox_list = [] + for item in imglist: + bbox_list.append(page.get_image_bbox(item, transform=True)) + infos = page.get_image_info(xrefs=True) + for im in infos: + bbox1 = im["bbox"] + transform1 = im["transform"] + match = False + for bbox2, transform2 in bbox_list: + abs_bbox = (bbox2 - bbox1).norm() + abs_matrix = (transform2 - transform1).norm() + if abs_bbox < 1e-4 and abs_matrix < 1e-4: + match = True + break + assert match diff --git a/tests/test_insertpdf.py b/tests/test_insertpdf.py index 2e861fbf1..091dcad4e 100644 --- a/tests/test_insertpdf.py +++ b/tests/test_insertpdf.py @@ -1,8 +1,8 @@ """ * Join multiple PDFs into a new one. -* Compare to stored earlier result: - - must be same length - - must differ b/o different ID fields +* Compare with stored earlier result: + - must have identical object definitions + - must have different trailers """ import os @@ -14,6 +14,7 @@ def test_joining(): + """Join 4 files and compare result with previously stored one.""" flist = ("1.pdf", "2.pdf", "3.pdf", "4.pdf") doc = fitz.open() for f in flist: @@ -22,7 +23,15 @@ def test_joining(): doc.insert_pdf(x, links=True, annots=True) x.close() - output = doc.tobytes(deflate=True, garbage=4) - old_output = open(oldfile, "rb").read() - assert len(output) == len(old_output) - assert output != old_output + tobytes = doc.tobytes(deflate=True, garbage=4) + new_output = fitz.open("pdf", tobytes) + old_output = fitz.open(oldfile) + # result must have same objects, because MuPDF garbage + # collection is a predictable process. + assert old_output.xref_length() == new_output.xref_length() + for xref in range(1, old_output.xref_length()): + assert old_output.xref_object(xref, compressed=True) == new_output.xref_object( + xref, compressed=True + ) + assert old_output.xref_get_keys(-1) == new_output.xref_get_keys(-1) + assert old_output.xref_get_key(-1, "ID") != new_output.xref_get_key(-1, "ID") diff --git a/tests/test_linequad.py b/tests/test_linequad.py index d7e21e3de..1306225be 100644 --- a/tests/test_linequad.py +++ b/tests/test_linequad.py @@ -1,5 +1,5 @@ """ -Check approx. equality of search quads versus recovered quads from +Check approx. equality of search quads versus quads recovered from text extractions. """ import os @@ -11,12 +11,17 @@ def test_quadcalc(): - text = " angle 327" + text = " angle 327" # search for this text doc = fitz.open(filename) page = doc[0] + # This special page has one block with one line, and + # its last span contains the searched text. block = page.get_text("dict", flags=0)["blocks"][0] line = block["lines"][0] + # compute quad of last span in line lineq = fitz.recover_line_quad(line, spans=line["spans"][-1:]) + + # let text search find the text returning quad coordinates rl = page.search_for(text, quads=True) searchq = rl[0] for i in range(4): diff --git a/tests/test_metadata.py b/tests/test_metadata.py index 48e632cb0..9538ef994 100644 --- a/tests/test_metadata.py +++ b/tests/test_metadata.py @@ -1,6 +1,6 @@ """ 1. Read metadata and compare with stored expected result. -2. Erase metadata and ensure object has indeed been emptied. +2. Erase metadata and assert object has indeed been deleted. """ import json import os @@ -19,6 +19,8 @@ def test_metadata(): def test_erase_meta(): doc.set_metadata({}) - info_str = doc.xref_get_key(-1, "Info")[1].split()[0] - info_xref = int(info_str) - assert doc.xref_object(info_xref, compressed=True) == "<<>>" + # Check PDF trailer and assert that there is no more /Info object + # or is set to "null". + statement1 = doc.xref_get_key(-1, "Info")[1] == "null" + statement2 = "Info" not in doc.xref_get_keys(-1) + assert statement2 or statement1 diff --git a/tests/test_nonpdf.py b/tests/test_nonpdf.py index d0083b810..530419e44 100644 --- a/tests/test_nonpdf.py +++ b/tests/test_nonpdf.py @@ -1,6 +1,6 @@ """ * Check EPUB document is no PDF -* Check page access using chapter notation +* Check page access using (chapter, page) notation * Re-layout EPUB ensuring a previous location is memorized """ import os @@ -26,6 +26,7 @@ def test_pageids(): def test_layout(): + """Memorize a page location, re-layout with ISO-A4, assert pre-determined location.""" loc = doc.make_bookmark((5, 11)) doc.layout(fitz.Rect(fitz.PaperRect("a4"))) assert doc.find_bookmark(loc) == (5, 6) diff --git a/tests/test_object_manipulation.py b/tests/test_object_manipulation.py index f20da7682..f3edf1b86 100644 --- a/tests/test_object_manipulation.py +++ b/tests/test_object_manipulation.py @@ -1,10 +1,16 @@ """ -Check low-level PDF object manipulation: +Check some low-level PDF object manipulations: 1. Set page rotation and compare with string in object definition. 2. Set page rotation via string manipulation and compare with result of - proper page.property. + proper page property. +3. Read the PDF trailer and verify it has the keys "/Root", "/ID", etc. """ import fitz +import os + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +resources = os.path.join(scriptdir, "resources") +filename = os.path.join(resources, "001003ED.pdf") def test_rotation1(): @@ -19,3 +25,14 @@ def test_rotation2(): page = doc.new_page() doc.xref_set_key(page.xref, "Rotate", "270") assert page.rotation == 270 + + +def test_trailer(): + """Access PDF trailer information.""" + doc = fitz.open(filename) + xreflen = doc.xref_length() + _, xreflen_str = doc.xref_get_key(-1, "Size") + assert xreflen == int(xreflen_str) + trailer_keys = doc.xref_get_keys(-1) + assert "ID" in trailer_keys + assert "Root" in trailer_keys diff --git a/tests/test_pagedelete.py b/tests/test_pagedelete.py new file mode 100644 index 000000000..dba09bb6f --- /dev/null +++ b/tests/test_pagedelete.py @@ -0,0 +1,62 @@ +""" +---------------------------------------------------- +This tests correct functioning of multi-page delete +---------------------------------------------------- +Create a PDF in memory with 100 pages with a unique text each. +Also create a TOC with a bookmark per page. +On every page after the first to-be-deleted page, also insert a link, which +points to this page. +The bookmark text equals the text on the page for easy verification. + +Then delete some pages and verify: +- the new TOC has empty items exactly for every deleted page +- the remaining TOC items still point to the correct page +- the document has no more links at all +""" +import fitz + +page_count = 100 # initial document length +r = range(5, 35, 5) # contains page numbers we will delete +# insert this link on pages after first deleted one +link = { + "from": fitz.Rect(100, 100, 120, 120), + "kind": fitz.LINK_GOTO, + "page": r[0], + "to": fitz.Point(100, 100), +} + + +def test_deletion(): + # First prepare the document. + doc = fitz.open() + toc = [] + for i in range(page_count): + page = doc.new_page() # make a page + page.insert_text((100, 100), "%i" % i) # insert unique text + if i > r[0]: # insert a link + page.insert_link(link) + toc.append([1, "%i" % i, i + 1]) # TOC bookmark to this page + + doc.set_toc(toc) # insert the TOC + assert doc.has_links() # check we did insert links + + # Test page deletion. + # Delete pages in range and verify result + doc.delete_pages(r) + assert not doc.has_links() # verify all links have gone + assert doc.page_count == page_count - len(r) # correct number deleted? + toc_new = doc.get_toc() # this is the modified TOC + # verify number of emptied items (have page number -1) + assert len([item for item in toc_new if item[-1] == -1]) == len(r) + # Deleted page numbers must correspond to TOC items with page number -1. + for i in r: + assert toc_new[i][-1] == -1 + # Remaining pages must be correctly pointed to by the non-empty TOC items + for item in toc_new: + pno = item[-1] + if pno == -1: # one of the emptied items + continue + pno -= 1 # PDF page number + text = doc[pno].get_text().replace("\n", "") + # toc text must equal text on page + assert text == item[1] diff --git a/tests/test_pagelabels.py b/tests/test_pagelabels.py index 7bff4fa95..93d869b5c 100644 --- a/tests/test_pagelabels.py +++ b/tests/test_pagelabels.py @@ -6,6 +6,7 @@ def make_doc(): + """Makes a PDF with 10 pages.""" doc = fitz.open() for i in range(10): page = doc.new_page() @@ -13,6 +14,10 @@ def make_doc(): def make_labels(): + """Return page label range rules. + - Rule 1: labels like "A-n", page 0 is first and has "A-1". + - Rule 2: labels as capital Roman numbers, page 4 is first and has "I". + """ return [ {"startpage": 0, "prefix": "A-", "style": "D", "firstpagenum": 1}, {"startpage": 4, "prefix": "", "style": "R", "firstpagenum": 1}, @@ -20,6 +25,12 @@ def make_labels(): def test_setlabels(): + """Check setting and inquiring page labels. + - Make a PDF with 10 pages + - Label pages + - Inquire labels of pages + - Get list of page numbers for a given label. + """ doc = make_doc() doc.set_page_labels(make_labels()) page_labels = [p.get_label() for p in doc] diff --git a/tests/test_pixmap.py b/tests/test_pixmap.py index b8de4a117..99524ebeb 100644 --- a/tests/test_pixmap.py +++ b/tests/test_pixmap.py @@ -1,6 +1,6 @@ """ Pixmap tests -* make pixmap of a page and confirm equality of bbox +* make pixmap of a page and assert bbox size * make pixmap from a PDF xref and compare with extracted image * pixmap from file and from binary image and compare """ @@ -28,8 +28,11 @@ def test_pagepixmap(): def test_pdfpixmap(): # pixmap from xref in a PDF doc = fitz.open(pdf) + # take first image item of first page img = doc.get_page_images(0)[0] + # make pixmap of it pix = fitz.Pixmap(doc, img[0]) + # assert pixmap properties assert pix.width == img[2] assert pix.height == img[3] # extract image and compare metadata @@ -40,6 +43,7 @@ def test_pdfpixmap(): def test_filepixmap(): # pixmaps from file and from stream + # should lead to same result pix1 = fitz.Pixmap(imgfile) stream = open(imgfile, "rb").read() pix2 = fitz.Pixmap(stream) diff --git a/tests/test_showpdfpage.py b/tests/test_showpdfpage.py index 31305143a..e7d602b0c 100644 --- a/tests/test_showpdfpage.py +++ b/tests/test_showpdfpage.py @@ -1,9 +1,9 @@ """ Tests: * Convert some image to a PDF - * Insert it in a PDF page in a given rectangle - * Ensure PDF Form XObject has been created - * Ensure inserted PDF is inside given retangle + * Insert it in a PDF page in given rectangle + * Assert PDF Form XObject has been created + * Assert inserted PDF is inside given retangle """ import os @@ -16,14 +16,16 @@ def test_insert(): doc = fitz.open() page = doc.new_page() - r1 = fitz.Rect(50, 50, 100, 100) - img = fitz.open(imgfile) - tobytes = img.convert_to_pdf() - src = fitz.open("pdf", tobytes) - page.show_pdf_page(r1, src, 0) - img = page.get_images(True)[0] - assert img[-1] > 0 - img = page.get_image_info()[0] - # we need to round somewhat here: + r1 = fitz.Rect(50, 50, 100, 100) # insert in here + img = fitz.open(imgfile) # open image + tobytes = img.convert_to_pdf() # get its PDF version (bytes object) + src = fitz.open("pdf", tobytes) # open as PDF + page.show_pdf_page(r1, src, 0) # insert in rectangle + img = page.get_images(True)[0] # make full image list of the page + assert img[-1] > 0 # xref of Form XObject! + img = page.get_image_info()[0] # read the page's images + + # Multiple comutations may lead to rounding issues, so we need + # some generosity here: bbox = list(map(lambda x: round(x, 2), img["bbox"])) assert bbox in r1 diff --git a/tests/test_textbox.py b/tests/test_textbox.py index c80d39dc9..e18784c12 100644 --- a/tests/test_textbox.py +++ b/tests/test_textbox.py @@ -1,5 +1,5 @@ """ -Fill a given text in rectangle of some PDF page using +Fill a given text in a rectangle on some PDF page using 1. TextWriter object 2. Basic text output @@ -45,6 +45,7 @@ def test_textbox2(): text, align=fitz.TEXT_ALIGN_LEFT, fontsize=12, + color=blue, ) # check text containment assert page.get_text() == page.get_text(clip=rect)