From 57a26fabcb9833903862366ba34efccc290b3177 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Fri, 16 Oct 2020 16:11:10 +0530 Subject: [PATCH 01/22] Add pdftopng --- camelot/parsers/lattice.py | 11 ++---- setup.py | 70 +++++++++++++++++++------------------- 2 files changed, 37 insertions(+), 44 deletions(-) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 3fd5bbe..6b19efe 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -208,17 +208,10 @@ class Lattice(BaseParser): return t def _generate_image(self): - from ..ext.ghostscript import Ghostscript + from pdftopng import pdftopng self.imagename = "".join([self.rootname, ".png"]) - gs_call = "-q -sDEVICE=png16m -o {} -r{} {}".format( - self.imagename, self.resolution, self.filename - ) - gs_call = gs_call.encode().split() - null = open(os.devnull, "wb") - with Ghostscript(*gs_call, stdout=null) as gs: - pass - null.close() + pdftopng.convert(pdf_path=self.filename, png_path=self.rootname) def _generate_table_bbox(self): def scale_areas(areas): diff --git a/setup.py b/setup.py index 4ac7e0a..b883d5b 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,10 @@ requires = [ "tabulate>=0.8.9", ] -cv_requires = ["opencv-python>=3.4.2.17"] +base_requires = [ + 'opencv-python>=3.4.2.17', + 'pdftopng>=0.1.0' +] plot_requires = [ "matplotlib>=2.2.3", @@ -40,44 +43,41 @@ dev_requires = [ "sphinx-autobuild>=2021.3.14", ] -all_requires = cv_requires + plot_requires +all_requires = base_requires + plot_requires dev_requires = dev_requires + all_requires def setup_package(): - metadata = dict( - name=about["__title__"], - version=about["__version__"], - description=about["__description__"], - long_description=readme, - long_description_content_type="text/markdown", - url=about["__url__"], - author=about["__author__"], - author_email=about["__author_email__"], - license=about["__license__"], - packages=find_packages(exclude=("tests",)), - install_requires=requires, - extras_require={ - "all": all_requires, - "cv": cv_requires, - "dev": dev_requires, - "plot": plot_requires, - }, - entry_points={ - "console_scripts": [ - "camelot = camelot.cli:cli", - ], - }, - classifiers=[ - # Trove classifiers - # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers - "License :: OSI Approved :: MIT License", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - ], - ) + metadata = dict(name=about['__title__'], + version=about['__version__'], + description=about['__description__'], + long_description=readme, + long_description_content_type="text/markdown", + url=about['__url__'], + author=about['__author__'], + author_email=about['__author_email__'], + license=about['__license__'], + packages=find_packages(exclude=('tests',)), + install_requires=requires, + extras_require={ + 'all': all_requires, + 'base': base_requires, + 'dev': dev_requires, + 'plot': plot_requires + }, + entry_points={ + 'console_scripts': [ + 'camelot = camelot.cli:cli', + ], + }, + classifiers=[ + # Trove classifiers + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8' + ]) try: from setuptools import setup From 286b2d6a1ce97bc38d096c219ffb5c6eaf442520 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 00:47:38 +0530 Subject: [PATCH 02/22] Bump pdftopng version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b883d5b..1307fbe 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ requires = [ base_requires = [ 'opencv-python>=3.4.2.17', - 'pdftopng>=0.1.0' + 'pdftopng>=0.1.1' ] plot_requires = [ From fdade4502ef79a38d610e3dde606fee1cedad86d Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 01:09:45 +0530 Subject: [PATCH 03/22] Fix pdftopng usage Delete cache --- camelot/parsers/base.py | 1 + camelot/parsers/lattice.py | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/camelot/parsers/base.py b/camelot/parsers/base.py index 79be789..aeba056 100644 --- a/camelot/parsers/base.py +++ b/camelot/parsers/base.py @@ -17,3 +17,4 @@ class BaseParser(object): self.vertical_text = get_text_objects(self.layout, ltype="vertical_text") self.pdf_width, self.pdf_height = self.dimensions self.rootname, __ = os.path.splitext(self.filename) + self.imagename = "".join([self.rootname, ".png"]) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 6b19efe..dac072a 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -210,8 +210,7 @@ class Lattice(BaseParser): def _generate_image(self): from pdftopng import pdftopng - self.imagename = "".join([self.rootname, ".png"]) - pdftopng.convert(pdf_path=self.filename, png_path=self.rootname) + pdftopng.convert(pdf_path=self.filename, png_path=self.imagename) def _generate_table_bbox(self): def scale_areas(areas): From 8563a0954423b718fb09b611ec1d4ecefe798a30 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 01:58:45 +0530 Subject: [PATCH 04/22] Add image conversion backends --- camelot/backends/__init__.py | 3 ++ camelot/backends/ghostscript_backend.py | 17 +++++++ camelot/backends/image_conversion.py | 15 ++++++ camelot/backends/poppler_backend.py | 8 +++ camelot/parsers/lattice.py | 8 +-- setup.py | 67 ++++++++++++------------- 6 files changed, 80 insertions(+), 38 deletions(-) create mode 100644 camelot/backends/__init__.py create mode 100644 camelot/backends/ghostscript_backend.py create mode 100644 camelot/backends/image_conversion.py create mode 100644 camelot/backends/poppler_backend.py diff --git a/camelot/backends/__init__.py b/camelot/backends/__init__.py new file mode 100644 index 0000000..8d0b91e --- /dev/null +++ b/camelot/backends/__init__.py @@ -0,0 +1,3 @@ +# -*- coding: utf-8 -*- + +from .image_conversion import ImageConversionBackend diff --git a/camelot/backends/ghostscript_backend.py b/camelot/backends/ghostscript_backend.py new file mode 100644 index 0000000..e0c2f42 --- /dev/null +++ b/camelot/backends/ghostscript_backend.py @@ -0,0 +1,17 @@ +# -*- coding: utf-8 -*- + +import ghostscript + + +class GhostscriptBackend(object): + def convert(self, pdf_path, png_path, resolution=300): + gs_args = [ + "gs", + "-q", + "-sDEVICE=png16m", + "-o", + png_path, + f"-r{resolution}", + pdf_path, + ] + ghostscript.Ghostscript(*gs_args) diff --git a/camelot/backends/image_conversion.py b/camelot/backends/image_conversion.py new file mode 100644 index 0000000..6652414 --- /dev/null +++ b/camelot/backends/image_conversion.py @@ -0,0 +1,15 @@ +# -*- coding: utf-8 -*- + +from .poppler_backend import PopplerBackend +from .ghostscript_backend import GhostscriptBackend + +backends = {"poppler": PopplerBackend, "ghostscript": GhostscriptBackend} + + +class ImageConversionBackend(object): + def __init__(self, backend="poppler"): + self.backend = backend + + def convert(self, pdf_path, png_path): + converter = backends[self.backend]() + converter.convert(pdf_path, png_path) diff --git a/camelot/backends/poppler_backend.py b/camelot/backends/poppler_backend.py new file mode 100644 index 0000000..c806098 --- /dev/null +++ b/camelot/backends/poppler_backend.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- + +from pdftopng import pdftopng + + +class PopplerBackend(object): + def convert(self, pdf_path, png_path): + pdftopng.convert(pdf_path, png_path) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index dac072a..ff47bfc 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -29,6 +29,7 @@ from ..image_processing import ( find_contours, find_joints, ) +from ..backends import ImageConversionBackend logger = logging.getLogger("camelot") @@ -111,7 +112,7 @@ class Lattice(BaseParser): threshold_constant=-2, iterations=0, resolution=300, - **kwargs + **kwargs, ): self.table_regions = table_regions self.table_areas = table_areas @@ -208,9 +209,8 @@ class Lattice(BaseParser): return t def _generate_image(self): - from pdftopng import pdftopng - - pdftopng.convert(pdf_path=self.filename, png_path=self.imagename) + converter = ImageConversionBackend() + converter.convert(self.filename, self.imagename) def _generate_table_bbox(self): def scale_areas(areas): diff --git a/setup.py b/setup.py index 1307fbe..5e99ea8 100644 --- a/setup.py +++ b/setup.py @@ -24,10 +24,7 @@ requires = [ "tabulate>=0.8.9", ] -base_requires = [ - 'opencv-python>=3.4.2.17', - 'pdftopng>=0.1.1' -] +base_requires = ["ghostscript>=0.7", "opencv-python>=3.4.2.17", "pdftopng>=0.1.1"] plot_requires = [ "matplotlib>=2.2.3", @@ -48,36 +45,38 @@ dev_requires = dev_requires + all_requires def setup_package(): - metadata = dict(name=about['__title__'], - version=about['__version__'], - description=about['__description__'], - long_description=readme, - long_description_content_type="text/markdown", - url=about['__url__'], - author=about['__author__'], - author_email=about['__author_email__'], - license=about['__license__'], - packages=find_packages(exclude=('tests',)), - install_requires=requires, - extras_require={ - 'all': all_requires, - 'base': base_requires, - 'dev': dev_requires, - 'plot': plot_requires - }, - entry_points={ - 'console_scripts': [ - 'camelot = camelot.cli:cli', - ], - }, - classifiers=[ - # Trove classifiers - # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers - 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8' - ]) + metadata = dict( + name=about["__title__"], + version=about["__version__"], + description=about["__description__"], + long_description=readme, + long_description_content_type="text/markdown", + url=about["__url__"], + author=about["__author__"], + author_email=about["__author_email__"], + license=about["__license__"], + packages=find_packages(exclude=("tests",)), + install_requires=requires, + extras_require={ + "all": all_requires, + "base": base_requires, + "dev": dev_requires, + "plot": plot_requires, + }, + entry_points={ + "console_scripts": [ + "camelot = camelot.cli:cli", + ], + }, + classifiers=[ + # Trove classifiers + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + ], + ) try: from setuptools import setup From 4cebd684bac02b34ab12e2e6b652a9ffbca10bb3 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 02:06:16 +0530 Subject: [PATCH 05/22] Remove ext.ghostscript --- .coveragerc | 1 - camelot/ext/__init__.py | 0 camelot/ext/ghostscript/COPYING | 674 ---------------------------- camelot/ext/ghostscript/__init__.py | 98 ---- camelot/ext/ghostscript/_gsprint.py | 270 ----------- 5 files changed, 1043 deletions(-) delete mode 100644 camelot/ext/__init__.py delete mode 100644 camelot/ext/ghostscript/COPYING delete mode 100644 camelot/ext/ghostscript/__init__.py delete mode 100644 camelot/ext/ghostscript/_gsprint.py diff --git a/.coveragerc b/.coveragerc index f5a1e06..398ff08 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,3 +1,2 @@ [run] branch = True -omit = camelot/ext/* diff --git a/camelot/ext/__init__.py b/camelot/ext/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/camelot/ext/ghostscript/COPYING b/camelot/ext/ghostscript/COPYING deleted file mode 100644 index 94a9ed0..0000000 --- a/camelot/ext/ghostscript/COPYING +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/camelot/ext/ghostscript/__init__.py b/camelot/ext/ghostscript/__init__.py deleted file mode 100644 index 5816475..0000000 --- a/camelot/ext/ghostscript/__init__.py +++ /dev/null @@ -1,98 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -ghostscript - A Python interface for the Ghostscript interpreter C-API -""" -# -# Modifications 2018 by Vinayak Mehta -# Copyright 2010-2018 by Hartmut Goebel -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -from . import _gsprint as gs - - -__author__ = "Hartmut Goebel " -__copyright__ = "Copyright 2010-2018 by Hartmut Goebel " -__license__ = "GNU General Public License version 3 (GPL v3)" -__version__ = "0.6" - - -class __Ghostscript(object): - def __init__(self, instance, args, stdin=None, stdout=None, stderr=None): - self._initialized = False - self._callbacks = None - if stdin or stdout or stderr: - self.set_stdio(stdin, stdout, stderr) - rc = gs.init_with_args(instance, args) - self._initialized = True - if rc == gs.e_Quit: - self.exit() - - def __enter__(self): - return self - - def __exit__(self, *args): - self.exit() - - def set_stdio(self, stdin=None, stdout=None, stderr=None): - """Set stdin, stdout and stderr of the ghostscript interpreter. - - The ``stdin`` stream has to support the ``readline()`` - interface. The ``stdout`` and ``stderr`` streams have to - support the ``write()`` and ``flush()`` interface. - - Please note that this does not affect the input- and output- - streams of the devices. Esp. setting stdout does not allow - catching the devise-output even when using ``-sOutputFile=-``. - - """ - global __instance__ - self._callbacks = ( - stdin and gs._wrap_stdin(stdin) or None, - stdout and gs._wrap_stdout(stdout) or None, - stderr and gs._wrap_stderr(stderr) or None, - ) - gs.set_stdio(__instance__, *self._callbacks) - - def __del__(self): - self.exit() - - def exit(self): - global __instance__ - if self._initialized: - if __instance__ is not None: - gs.exit(__instance__) - gs.delete_instance(__instance__) - __instance__ = None - self._initialized = False - - -def Ghostscript(*args, **kwargs): - """Factory function for setting up a Ghostscript instance""" - global __instance__ - # Ghostscript only supports a single instance - if __instance__ is None: - __instance__ = gs.new_instance() - return __Ghostscript( - __instance__, - args, - stdin=kwargs.get("stdin", None), - stdout=kwargs.get("stdout", None), - stderr=kwargs.get("stderr", None), - ) - - -__instance__ = None diff --git a/camelot/ext/ghostscript/_gsprint.py b/camelot/ext/ghostscript/_gsprint.py deleted file mode 100644 index 9896805..0000000 --- a/camelot/ext/ghostscript/_gsprint.py +++ /dev/null @@ -1,270 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -ghostscript._gsprint - A low-level interface to the Ghostscript C-API using ctypes -""" -# -# Modifications 2018 by Vinayak Mehta -# Copyright 2010-2018 by Hartmut Goebel -# -# Display_callback Structure by Lasse Fister in 2013 -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU General Public License as published by -# the Free Software Foundation, either version 3 of the License, or -# (at your option) any later version. -# -# This program is distributed in the hope that it will be useful, but -# WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -# - -import sys -from ctypes import * - - -# base/gserrors.h -# -# Internal code for a normal exit when usage info is displayed. -# This allows Window versions of Ghostscript to pause until -# the message can be read. -# -e_Info = -110 - -# -# Internal code for the .quit operator. -# The real quit code is an integer on the operand stack. -# gs_interpret returns this only for a .quit with a zero exit code. -# -e_Quit = -101 - -__author__ = "Hartmut Goebel " -__copyright__ = "Copyright 2010-2018 by Hartmut Goebel " -__license__ = "GNU General Public License version 3 (GPL v3)" -__version__ = "0.6" - -gs_main_instance = c_void_p -display_callback = c_void_p - -# https://www.ghostscript.com/doc/current/API.htm - - -class GhostscriptError(Exception): - def __init__(self, ecode): - self.code = ecode - - -def new_instance(): - """Create a new instance of Ghostscript - - This instance is passed to most other API functions. - """ - # :todo: The caller_handle will be provided to callback functions. - display_callback = None - instance = gs_main_instance() - rc = libgs.gsapi_new_instance(pointer(instance), display_callback) - if rc != 0: - raise GhostscriptError(rc) - return instance - - -def delete_instance(instance): - """Destroy an instance of Ghostscript - - Before you call this, Ghostscript must have finished. - If Ghostscript has been initialised, you must call exit() - before delete_instance() - """ - return libgs.gsapi_delete_instance(instance) - - -if sys.platform == "win32": - c_stdstream_call_t = WINFUNCTYPE(c_int, gs_main_instance, POINTER(c_char), c_int) -else: - c_stdstream_call_t = CFUNCTYPE(c_int, gs_main_instance, POINTER(c_char), c_int) - - -def _wrap_stdin(infp): - """Wrap a filehandle into a C function to be used as `stdin` callback - for ``set_stdio``. The filehandle has to support the readline() method. - """ - - def _wrap(instance, dest, count): - try: - data = infp.readline(count) - except: - count = -1 - else: - if not data: - count = 0 - else: - count = len(data) - memmove(dest, c_char_p(data), count) - return count - - return c_stdstream_call_t(_wrap) - - -def _wrap_stdout(outfp): - """Wrap a filehandle into a C function to be used as `stdout` or - `stderr` callback for ``set_stdio``. The filehandle has to support the - write() and flush() methods. - """ - - def _wrap(instance, str, count): - outfp.write(str[:count]) - outfp.flush() - return count - - return c_stdstream_call_t(_wrap) - - -_wrap_stderr = _wrap_stdout - - -def set_stdio(instance, stdin, stdout, stderr): - """Set the callback functions for stdio. - - ``stdin``, ``stdout`` and ``stderr`` have to be ``ctypes`` - callback functions matching the ``_gsprint.c_stdstream_call_t`` - prototype. You may want to use _wrap_* to wrap file handles. - - Note 1: This function only changes stdio of the Postscript - interpreter, not that of the devices. - - Note 2: Make sure you keep references to C function objects - as long as they are used from C code. Otherwise they may be - garbage collected, crashing your program when a callback is made. - - The ``stdin`` callback function should return the number of - characters read, `0` for EOF, or `-1` for error. The `stdout` and - `stderr` callback functions should return the number of characters - written. - - You may pass ``None`` for any of stdin, stdout or stderr , in which - case the system stdin, stdout resp. stderr will be used. - """ - rc = libgs.gsapi_set_stdio(instance, stdin, stdout, stderr) - if rc not in (0, e_Quit, e_Info): - raise GhostscriptError(rc) - return rc - - -def init_with_args(instance, argv): - """Initialise the interpreter - - 1. If quit or EOF occur during init_with_args(), the return value - will be e_Quit. This is not an error. You must call exit() and - must not call any other functions. - - 2. If usage info should be displayed, the return value will be - e_Info which is not an error. Do not call exit(). - - 3. Under normal conditions this returns 0. You would then call one - or more run_*() functions and then finish with exit() - """ - ArgArray = c_char_p * len(argv) - c_argv = ArgArray(*argv) - rc = libgs.gsapi_init_with_args(instance, len(argv), c_argv) - if rc not in (0, e_Quit, e_Info): - raise GhostscriptError(rc) - return rc - - -def exit(instance): - """Exit the interpreter - - This must be called on shutdown if init_with_args() has been - called, and just before delete_instance() - """ - rc = libgs.gsapi_exit(instance) - if rc != 0: - raise GhostscriptError(rc) - return rc - - -def __win32_finddll(): - try: - import winreg - except ImportError: - # assume Python 2 - from _winreg import ( - OpenKey, - CloseKey, - EnumKey, - QueryValueEx, - QueryInfoKey, - HKEY_LOCAL_MACHINE, - ) - else: - from winreg import ( - OpenKey, - CloseKey, - EnumKey, - QueryValueEx, - QueryInfoKey, - HKEY_LOCAL_MACHINE, - ) - - from distutils.version import LooseVersion - import os - - dlls = [] - # Look up different variants of Ghostscript and take the highest - # version for which the DLL is to be found in the filesystem. - for key_name in ( - "AFPL Ghostscript", - "Aladdin Ghostscript", - "GNU Ghostscript", - "GPL Ghostscript", - ): - try: - k1 = OpenKey(HKEY_LOCAL_MACHINE, "Software\\%s" % key_name) - for num in range(0, QueryInfoKey(k1)[0]): - version = EnumKey(k1, num) - try: - k2 = OpenKey(k1, version) - dll_path = QueryValueEx(k2, "GS_DLL")[0] - CloseKey(k2) - if os.path.exists(dll_path): - dlls.append((LooseVersion(version), dll_path)) - except WindowsError: - pass - CloseKey(k1) - except WindowsError: - pass - if dlls: - dlls.sort() - return dlls[-1][-1] - else: - return None - - -if sys.platform == "win32": - libgs = __win32_finddll() - if not libgs: - import ctypes.util - - libgs = ctypes.util.find_library( - "".join(("gsdll", str(ctypes.sizeof(ctypes.c_voidp) * 8), ".dll")) - ) # finds in %PATH% - if not libgs: - raise RuntimeError("Please make sure that Ghostscript is installed") - libgs = windll.LoadLibrary(libgs) -else: - try: - libgs = cdll.LoadLibrary("libgs.so") - except OSError: - # shared object file not found - import ctypes.util - - libgs = ctypes.util.find_library("gs") - if not libgs: - raise RuntimeError("Please make sure that Ghostscript is installed") - libgs = cdll.LoadLibrary(libgs) - -del __win32_finddll From a96702987ff350d6c811088b457f45adae6abf40 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 02:20:44 +0530 Subject: [PATCH 06/22] Raise error if ghostscript not installed --- camelot/backends/ghostscript_backend.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/camelot/backends/ghostscript_backend.py b/camelot/backends/ghostscript_backend.py index e0c2f42..cba3e83 100644 --- a/camelot/backends/ghostscript_backend.py +++ b/camelot/backends/ghostscript_backend.py @@ -1,10 +1,40 @@ # -*- coding: utf-8 -*- +import sys +import ctypes +from ctypes.util import find_library + import ghostscript +def installed_posix(): + library = find_library("gs") + return library is not None + + +def installed_windows(): + library = find_library( + "".join(("gsdll", str(ctypes.sizeof(ctypes.c_voidp) * 8), ".dll")) + ) + return library is not None + + class GhostscriptBackend(object): + def installed(self): + if sys.platform in ["linux", "darwin"]: + return installed_posix() + elif sys.platform == "win32": + return installed_windows() + else: + return installed_posix() + def convert(self, pdf_path, png_path, resolution=300): + if not self.installed(): + raise OSError( + "Ghostscript is not installed. Please install it using the instructions" + "here: https://camelot-py.readthedocs.io/en/master/user/install-deps.html" + ) + gs_args = [ "gs", "-q", From 3e4e848a09c1404b30d0f15432213ba499290f5f Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 03:16:54 +0530 Subject: [PATCH 07/22] Add fallbacks to image conversion --- camelot/backends/image_conversion.py | 32 +++++++++++++++++++++++++--- camelot/parsers/lattice.py | 8 +++---- 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/camelot/backends/image_conversion.py b/camelot/backends/image_conversion.py index 6652414..9b07fa0 100644 --- a/camelot/backends/image_conversion.py +++ b/camelot/backends/image_conversion.py @@ -1,15 +1,41 @@ # -*- coding: utf-8 -*- +import logging + from .poppler_backend import PopplerBackend from .ghostscript_backend import GhostscriptBackend +logger = logging.getLogger("camelot") backends = {"poppler": PopplerBackend, "ghostscript": GhostscriptBackend} class ImageConversionBackend(object): - def __init__(self, backend="poppler"): + def __init__(self, backend="poppler", use_fallback=True): + if backend not in backends.keys(): + raise ValueError(f"Image conversion backend '{backend}' not supported") + self.backend = backend + self.use_fallback = use_fallback + self.fallbacks = list(filter(lambda x: x != backend, backends.keys())) def convert(self, pdf_path, png_path): - converter = backends[self.backend]() - converter.convert(pdf_path, png_path) + try: + converter = backends[self.backend]() + converter.convert(pdf_path, png_path) + except Exception as e: + logger.info(f"Image conversion backend '{self.backend}' failed with {str(e)}") + + if self.use_fallback: + for fallback in self.fallbacks: + logger.info(f"Falling back on '{fallback}'") + + try: + converter = backends[self.backend]() + converter.convert(pdf_path, png_path) + except Exception as e: + logger.info(f"Image conversion backend '{fallback}' failed with {str(e)}") + + continue + else: + logger.info(f"Image conversion backend '{fallback}' succeeded") + break diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index ff47bfc..50530cc 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -129,6 +129,7 @@ class Lattice(BaseParser): self.threshold_constant = threshold_constant self.iterations = iterations self.resolution = resolution + self.backend = ImageConversionBackend() @staticmethod def _reduce_index(t, idx, shift_text): @@ -208,10 +209,6 @@ class Lattice(BaseParser): t.cells[i][j].text = t.cells[i - 1][j].text return t - def _generate_image(self): - converter = ImageConversionBackend() - converter.convert(self.filename, self.imagename) - def _generate_table_bbox(self): def scale_areas(areas): scaled_areas = [] @@ -391,7 +388,8 @@ class Lattice(BaseParser): ) return [] - self._generate_image() + self.backend.convert(self.filename, self.imagename) + self._generate_table_bbox() _tables = [] From 0c5aff60b409d0da80806c8182e10f32ea9c1545 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 03:19:04 +0530 Subject: [PATCH 08/22] Remove newline Delete cache --- camelot/backends/image_conversion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/camelot/backends/image_conversion.py b/camelot/backends/image_conversion.py index 9b07fa0..3ab88c1 100644 --- a/camelot/backends/image_conversion.py +++ b/camelot/backends/image_conversion.py @@ -34,7 +34,6 @@ class ImageConversionBackend(object): converter.convert(pdf_path, png_path) except Exception as e: logger.info(f"Image conversion backend '{fallback}' failed with {str(e)}") - continue else: logger.info(f"Image conversion backend '{fallback}' succeeded") From 57034b88b0bfeb5a4452b9739984f97003368449 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 03:20:06 +0530 Subject: [PATCH 09/22] Update .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index d0aea62..aaeac14 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +fontconfig/ __pycache__/ *.py[cod] *.so From 3ddc02b2f2fce87e36c6d4c0660ee0ad6aa7031b Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 03:24:15 +0530 Subject: [PATCH 10/22] Fix fallback key --- camelot/backends/image_conversion.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/camelot/backends/image_conversion.py b/camelot/backends/image_conversion.py index 3ab88c1..9aeaecf 100644 --- a/camelot/backends/image_conversion.py +++ b/camelot/backends/image_conversion.py @@ -23,17 +23,17 @@ class ImageConversionBackend(object): converter = backends[self.backend]() converter.convert(pdf_path, png_path) except Exception as e: - logger.info(f"Image conversion backend '{self.backend}' failed with {str(e)}") + logger.info(f"Image conversion backend '{self.backend}' failed with '{str(e)}'") if self.use_fallback: for fallback in self.fallbacks: logger.info(f"Falling back on '{fallback}'") try: - converter = backends[self.backend]() + converter = backends[fallback]() converter.convert(pdf_path, png_path) except Exception as e: - logger.info(f"Image conversion backend '{fallback}' failed with {str(e)}") + logger.info(f"Image conversion backend '{fallback}' failed with '{str(e)}'") continue else: logger.info(f"Image conversion backend '{fallback}' succeeded") From 36dcfe99d83580200a765224dbcfdb8819e2ec23 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 28 Jun 2021 05:32:48 +0530 Subject: [PATCH 11/22] Split tests for lattice and stream, and fix test_common reprs --- camelot/backends/image_conversion.py | 8 +- camelot/core.py | 8 +- camelot/parsers/lattice.py | 3 +- tests/test_common.py | 293 ++++++--------------------- tests/test_lattice.py | 104 ++++++++++ tests/test_stream.py | 132 ++++++++++++ 6 files changed, 314 insertions(+), 234 deletions(-) create mode 100644 tests/test_lattice.py create mode 100644 tests/test_stream.py diff --git a/camelot/backends/image_conversion.py b/camelot/backends/image_conversion.py index 9aeaecf..004bce7 100644 --- a/camelot/backends/image_conversion.py +++ b/camelot/backends/image_conversion.py @@ -23,7 +23,9 @@ class ImageConversionBackend(object): converter = backends[self.backend]() converter.convert(pdf_path, png_path) except Exception as e: - logger.info(f"Image conversion backend '{self.backend}' failed with '{str(e)}'") + logger.info( + f"Image conversion backend '{self.backend}' failed with '{str(e)}'" + ) if self.use_fallback: for fallback in self.fallbacks: @@ -33,7 +35,9 @@ class ImageConversionBackend(object): converter = backends[fallback]() converter.convert(pdf_path, png_path) except Exception as e: - logger.info(f"Image conversion backend '{fallback}' failed with '{str(e)}'") + logger.info( + f"Image conversion backend '{fallback}' failed with '{str(e)}'" + ) continue else: logger.info(f"Image conversion backend '{fallback}' succeeded") diff --git a/camelot/core.py b/camelot/core.py index 63ddc15..58a98ef 100644 --- a/camelot/core.py +++ b/camelot/core.py @@ -288,10 +288,10 @@ class Cell(object): self._text = "" def __repr__(self): - x1 = round(self.x1, 2) - y1 = round(self.y1, 2) - x2 = round(self.x2, 2) - y2 = round(self.y2, 2) + x1 = round(self.x1) + y1 = round(self.y1) + x2 = round(self.x2) + y2 = round(self.y2) return f"" @property diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 50530cc..937e867 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -112,6 +112,7 @@ class Lattice(BaseParser): threshold_constant=-2, iterations=0, resolution=300, + backend=ImageConversionBackend(), **kwargs, ): self.table_regions = table_regions @@ -129,7 +130,7 @@ class Lattice(BaseParser): self.threshold_constant = threshold_constant self.iterations = iterations self.resolution = resolution - self.backend = ImageConversionBackend() + self.backend = backend @staticmethod def _reduce_index(t, idx, shift_text): diff --git a/tests/test_common.py b/tests/test_common.py index cb9a968..ddb8de2 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -8,6 +8,7 @@ from pandas.testing import assert_frame_equal import camelot from camelot.core import Table, TableList from camelot.__version__ import generate_version +from camelot.backends import ImageConversionBackend from .data import * @@ -15,6 +16,21 @@ testdir = os.path.dirname(os.path.abspath(__file__)) testdir = os.path.join(testdir, "files") +def test_version_generation(): + version = (0, 7, 3) + assert generate_version(version, prerelease=None, revision=None) == "0.7.3" + + +def test_version_generation_with_prerelease_revision(): + version = (0, 7, 3) + prerelease = "alpha" + revision = 2 + assert ( + generate_version(version, prerelease=prerelease, revision=revision) + == "0.7.3-alpha.2" + ) + + def test_parsing_report(): parsing_report = {"accuracy": 99.02, "whitespace": 12.24, "order": 1, "page": 1} @@ -34,246 +50,92 @@ def test_password(): assert_frame_equal(df, tables[0].df) -def test_stream(): - df = pd.DataFrame(data_stream) - - filename = os.path.join(testdir, "health.pdf") - tables = camelot.read_pdf(filename, flavor="stream") - assert_frame_equal(df, tables[0].df) - - -def test_stream_table_rotated(): - df = pd.DataFrame(data_stream_table_rotated) - - filename = os.path.join(testdir, "clockwise_table_2.pdf") - tables = camelot.read_pdf(filename, flavor="stream") - assert_frame_equal(df, tables[0].df) - - filename = os.path.join(testdir, "anticlockwise_table_2.pdf") - tables = camelot.read_pdf(filename, flavor="stream") - assert_frame_equal(df, tables[0].df) - - -def test_stream_two_tables(): - df1 = pd.DataFrame(data_stream_two_tables_1) - df2 = pd.DataFrame(data_stream_two_tables_2) - - filename = os.path.join(testdir, "tabula/12s0324.pdf") - tables = camelot.read_pdf(filename, flavor="stream") - - assert len(tables) == 2 - assert df1.equals(tables[0].df) - assert df2.equals(tables[1].df) - - -def test_stream_table_regions(): - df = pd.DataFrame(data_stream_table_areas) - - filename = os.path.join(testdir, "tabula/us-007.pdf") - tables = camelot.read_pdf( - filename, flavor="stream", table_regions=["320,460,573,335"] - ) - assert_frame_equal(df, tables[0].df) - - -def test_stream_table_areas(): - df = pd.DataFrame(data_stream_table_areas) - - filename = os.path.join(testdir, "tabula/us-007.pdf") - tables = camelot.read_pdf( - filename, flavor="stream", table_areas=["320,500,573,335"] - ) - assert_frame_equal(df, tables[0].df) - - -def test_stream_columns(): - df = pd.DataFrame(data_stream_columns) - - filename = os.path.join(testdir, "mexican_towns.pdf") - tables = camelot.read_pdf( - filename, flavor="stream", columns=["67,180,230,425,475"], row_tol=10 - ) - assert_frame_equal(df, tables[0].df) - - -def test_stream_split_text(): - df = pd.DataFrame(data_stream_split_text) - - filename = os.path.join(testdir, "tabula/m27.pdf") - tables = camelot.read_pdf( - filename, - flavor="stream", - columns=["72,95,209,327,442,529,566,606,683"], - split_text=True, - ) - assert_frame_equal(df, tables[0].df) - - -def test_stream_flag_size(): - df = pd.DataFrame(data_stream_flag_size) - - filename = os.path.join(testdir, "superscript.pdf") - tables = camelot.read_pdf(filename, flavor="stream", flag_size=True) - assert_frame_equal(df, tables[0].df) - - -def test_stream_strip_text(): - df = pd.DataFrame(data_stream_strip_text) - - filename = os.path.join(testdir, "detect_vertical_false.pdf") - tables = camelot.read_pdf(filename, flavor="stream", strip_text=" ,\n") - assert_frame_equal(df, tables[0].df) - - -def test_stream_edge_tol(): - df = pd.DataFrame(data_stream_edge_tol) - - filename = os.path.join(testdir, "edge_tol.pdf") - tables = camelot.read_pdf(filename, flavor="stream", edge_tol=500) - assert_frame_equal(df, tables[0].df) - - -def test_stream_layout_kwargs(): - df = pd.DataFrame(data_stream_layout_kwargs) - - filename = os.path.join(testdir, "detect_vertical_false.pdf") - tables = camelot.read_pdf( - filename, flavor="stream", layout_kwargs={"detect_vertical": False} - ) - assert_frame_equal(df, tables[0].df) - - -def test_lattice(): - df = pd.DataFrame(data_lattice) - - filename = os.path.join( - testdir, "tabula/icdar2013-dataset/competition-dataset-us/us-030.pdf" - ) - tables = camelot.read_pdf(filename, pages="2") - assert_frame_equal(df, tables[0].df) - - -def test_lattice_table_rotated(): - df = pd.DataFrame(data_lattice_table_rotated) - - filename = os.path.join(testdir, "clockwise_table_1.pdf") - tables = camelot.read_pdf(filename) - assert_frame_equal(df, tables[0].df) - - filename = os.path.join(testdir, "anticlockwise_table_1.pdf") - tables = camelot.read_pdf(filename) - assert_frame_equal(df, tables[0].df) - - -def test_lattice_two_tables(): - df1 = pd.DataFrame(data_lattice_two_tables_1) - df2 = pd.DataFrame(data_lattice_two_tables_2) - - filename = os.path.join(testdir, "twotables_2.pdf") - tables = camelot.read_pdf(filename) - assert len(tables) == 2 - assert df1.equals(tables[0].df) - assert df2.equals(tables[1].df) - - -def test_lattice_table_regions(): - df = pd.DataFrame(data_lattice_table_regions) - - filename = os.path.join(testdir, "table_region.pdf") - tables = camelot.read_pdf(filename, table_regions=["170,370,560,270"]) - assert_frame_equal(df, tables[0].df) - - -def test_lattice_table_areas(): - df = pd.DataFrame(data_lattice_table_areas) - - filename = os.path.join(testdir, "twotables_2.pdf") - tables = camelot.read_pdf(filename, table_areas=["80,693,535,448"]) - assert_frame_equal(df, tables[0].df) - - -def test_lattice_process_background(): - df = pd.DataFrame(data_lattice_process_background) - - filename = os.path.join(testdir, "background_lines_1.pdf") - tables = camelot.read_pdf(filename, process_background=True) - assert_frame_equal(df, tables[1].df) - - -def test_lattice_copy_text(): - df = pd.DataFrame(data_lattice_copy_text) - - filename = os.path.join(testdir, "row_span_1.pdf") - tables = camelot.read_pdf(filename, line_scale=60, copy_text="v") - assert_frame_equal(df, tables[0].df) - - -def test_lattice_shift_text(): - df_lt = pd.DataFrame(data_lattice_shift_text_left_top) - df_disable = pd.DataFrame(data_lattice_shift_text_disable) - df_rb = pd.DataFrame(data_lattice_shift_text_right_bottom) - - filename = os.path.join(testdir, "column_span_2.pdf") - tables = camelot.read_pdf(filename, line_scale=40) - assert df_lt.equals(tables[0].df) - - tables = camelot.read_pdf(filename, line_scale=40, shift_text=[""]) - assert df_disable.equals(tables[0].df) - - tables = camelot.read_pdf(filename, line_scale=40, shift_text=["r", "b"]) - assert df_rb.equals(tables[0].df) - - -def test_repr(): +def test_repr_poppler(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) assert repr(tables) == "" assert repr(tables[0]) == "" assert ( - repr(tables[0].cells[0][0]) == "" + repr(tables[0].cells[0][0]) == "" ) -def test_pages(): +def test_repr_ghostscript(): + filename = os.path.join(testdir, "foo.pdf") + tables = camelot.read_pdf(filename, backend=ImageConversionBackend(backend="ghostscript")) + assert repr(tables) == "" + assert repr(tables[0]) == "
" + assert ( + repr(tables[0].cells[0][0]) == "" + ) + + +def test_url_poppler(): url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" tables = camelot.read_pdf(url) assert repr(tables) == "" assert repr(tables[0]) == "
" assert ( - repr(tables[0].cells[0][0]) == "" + repr(tables[0].cells[0][0]) == "" + ) + + +def test_url_ghostscript(): + url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" + tables = camelot.read_pdf(url, backend=ImageConversionBackend(backend="ghostscript")) + assert repr(tables) == "" + assert repr(tables[0]) == "
" + assert ( + repr(tables[0].cells[0][0]) == "" + ) + + +def test_pages_poppler(): + url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" + tables = camelot.read_pdf(url) + assert repr(tables) == "" + assert repr(tables[0]) == "
" + assert ( + repr(tables[0].cells[0][0]) == "" ) tables = camelot.read_pdf(url, pages="1-end") assert repr(tables) == "" assert repr(tables[0]) == "
" assert ( - repr(tables[0].cells[0][0]) == "" + repr(tables[0].cells[0][0]) == "" ) tables = camelot.read_pdf(url, pages="all") assert repr(tables) == "" assert repr(tables[0]) == "
" assert ( - repr(tables[0].cells[0][0]) == "" + repr(tables[0].cells[0][0]) == "" ) -def test_url(): +def test_pages_ghostscript(): url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" tables = camelot.read_pdf(url) assert repr(tables) == "" assert repr(tables[0]) == "
" assert ( - repr(tables[0].cells[0][0]) == "" + repr(tables[0].cells[0][0]) == "" ) + tables = camelot.read_pdf(url, pages="1-end") + assert repr(tables) == "" + assert repr(tables[0]) == "
" + assert ( + repr(tables[0].cells[0][0]) == "" + ) -def test_arabic(): - df = pd.DataFrame(data_arabic) - - filename = os.path.join(testdir, "tabula/arabic.pdf") - tables = camelot.read_pdf(filename) - assert_frame_equal(df, tables[0].df) + tables = camelot.read_pdf(url, pages="all") + assert repr(tables) == "" + assert repr(tables[0]) == "
" + assert ( + repr(tables[0].cells[0][0]) == "" + ) def test_table_order(): @@ -299,26 +161,3 @@ def test_table_order(): (1, 2), (1, 1), ] - - -def test_version_generation(): - version = (0, 7, 3) - assert generate_version(version, prerelease=None, revision=None) == "0.7.3" - - -def test_version_generation_with_prerelease_revision(): - version = (0, 7, 3) - prerelease = "alpha" - revision = 2 - assert ( - generate_version(version, prerelease=prerelease, revision=revision) - == "0.7.3-alpha.2" - ) - - -def test_stream_duplicated_text(): - df = pd.DataFrame(data_stream_duplicated_text) - - filename = os.path.join(testdir, "birdisland.pdf") - tables = camelot.read_pdf(filename, flavor="stream") - assert_frame_equal(df, tables[0].df) diff --git a/tests/test_lattice.py b/tests/test_lattice.py new file mode 100644 index 0000000..7706b4a --- /dev/null +++ b/tests/test_lattice.py @@ -0,0 +1,104 @@ +# -*- coding: utf-8 -*- + +import os + +import pandas as pd +from pandas.testing import assert_frame_equal + +import camelot +from camelot.core import Table, TableList +from camelot.__version__ import generate_version + +from .data import * + +testdir = os.path.dirname(os.path.abspath(__file__)) +testdir = os.path.join(testdir, "files") + + +def test_lattice(): + df = pd.DataFrame(data_lattice) + + filename = os.path.join( + testdir, "tabula/icdar2013-dataset/competition-dataset-us/us-030.pdf" + ) + tables = camelot.read_pdf(filename, pages="2") + assert_frame_equal(df, tables[0].df) + + +def test_lattice_table_rotated(): + df = pd.DataFrame(data_lattice_table_rotated) + + filename = os.path.join(testdir, "clockwise_table_1.pdf") + tables = camelot.read_pdf(filename) + assert_frame_equal(df, tables[0].df) + + filename = os.path.join(testdir, "anticlockwise_table_1.pdf") + tables = camelot.read_pdf(filename) + assert_frame_equal(df, tables[0].df) + + +def test_lattice_two_tables(): + df1 = pd.DataFrame(data_lattice_two_tables_1) + df2 = pd.DataFrame(data_lattice_two_tables_2) + + filename = os.path.join(testdir, "twotables_2.pdf") + tables = camelot.read_pdf(filename) + assert len(tables) == 2 + assert df1.equals(tables[0].df) + assert df2.equals(tables[1].df) + + +def test_lattice_table_regions(): + df = pd.DataFrame(data_lattice_table_regions) + + filename = os.path.join(testdir, "table_region.pdf") + tables = camelot.read_pdf(filename, table_regions=["170,370,560,270"]) + assert_frame_equal(df, tables[0].df) + + +def test_lattice_table_areas(): + df = pd.DataFrame(data_lattice_table_areas) + + filename = os.path.join(testdir, "twotables_2.pdf") + tables = camelot.read_pdf(filename, table_areas=["80,693,535,448"]) + assert_frame_equal(df, tables[0].df) + + +def test_lattice_process_background(): + df = pd.DataFrame(data_lattice_process_background) + + filename = os.path.join(testdir, "background_lines_1.pdf") + tables = camelot.read_pdf(filename, process_background=True) + assert_frame_equal(df, tables[1].df) + + +def test_lattice_copy_text(): + df = pd.DataFrame(data_lattice_copy_text) + + filename = os.path.join(testdir, "row_span_1.pdf") + tables = camelot.read_pdf(filename, line_scale=60, copy_text="v") + assert_frame_equal(df, tables[0].df) + + +def test_lattice_shift_text(): + df_lt = pd.DataFrame(data_lattice_shift_text_left_top) + df_disable = pd.DataFrame(data_lattice_shift_text_disable) + df_rb = pd.DataFrame(data_lattice_shift_text_right_bottom) + + filename = os.path.join(testdir, "column_span_2.pdf") + tables = camelot.read_pdf(filename, line_scale=40) + assert df_lt.equals(tables[0].df) + + tables = camelot.read_pdf(filename, line_scale=40, shift_text=[""]) + assert df_disable.equals(tables[0].df) + + tables = camelot.read_pdf(filename, line_scale=40, shift_text=["r", "b"]) + assert df_rb.equals(tables[0].df) + + +def test_lattice_arabic(): + df = pd.DataFrame(data_arabic) + + filename = os.path.join(testdir, "tabula/arabic.pdf") + tables = camelot.read_pdf(filename) + assert_frame_equal(df, tables[0].df) diff --git a/tests/test_stream.py b/tests/test_stream.py new file mode 100644 index 0000000..4a0ec0c --- /dev/null +++ b/tests/test_stream.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- + +import os + +import pandas as pd +from pandas.testing import assert_frame_equal + +import camelot +from camelot.core import Table, TableList +from camelot.__version__ import generate_version + +from .data import * + +testdir = os.path.dirname(os.path.abspath(__file__)) +testdir = os.path.join(testdir, "files") + + +def test_stream(): + df = pd.DataFrame(data_stream) + + filename = os.path.join(testdir, "health.pdf") + tables = camelot.read_pdf(filename, flavor="stream") + assert_frame_equal(df, tables[0].df) + + +def test_stream_table_rotated(): + df = pd.DataFrame(data_stream_table_rotated) + + filename = os.path.join(testdir, "clockwise_table_2.pdf") + tables = camelot.read_pdf(filename, flavor="stream") + assert_frame_equal(df, tables[0].df) + + filename = os.path.join(testdir, "anticlockwise_table_2.pdf") + tables = camelot.read_pdf(filename, flavor="stream") + assert_frame_equal(df, tables[0].df) + + +def test_stream_two_tables(): + df1 = pd.DataFrame(data_stream_two_tables_1) + df2 = pd.DataFrame(data_stream_two_tables_2) + + filename = os.path.join(testdir, "tabula/12s0324.pdf") + tables = camelot.read_pdf(filename, flavor="stream") + + assert len(tables) == 2 + assert df1.equals(tables[0].df) + assert df2.equals(tables[1].df) + + +def test_stream_table_regions(): + df = pd.DataFrame(data_stream_table_areas) + + filename = os.path.join(testdir, "tabula/us-007.pdf") + tables = camelot.read_pdf( + filename, flavor="stream", table_regions=["320,460,573,335"] + ) + assert_frame_equal(df, tables[0].df) + + +def test_stream_table_areas(): + df = pd.DataFrame(data_stream_table_areas) + + filename = os.path.join(testdir, "tabula/us-007.pdf") + tables = camelot.read_pdf( + filename, flavor="stream", table_areas=["320,500,573,335"] + ) + assert_frame_equal(df, tables[0].df) + + +def test_stream_columns(): + df = pd.DataFrame(data_stream_columns) + + filename = os.path.join(testdir, "mexican_towns.pdf") + tables = camelot.read_pdf( + filename, flavor="stream", columns=["67,180,230,425,475"], row_tol=10 + ) + assert_frame_equal(df, tables[0].df) + + +def test_stream_split_text(): + df = pd.DataFrame(data_stream_split_text) + + filename = os.path.join(testdir, "tabula/m27.pdf") + tables = camelot.read_pdf( + filename, + flavor="stream", + columns=["72,95,209,327,442,529,566,606,683"], + split_text=True, + ) + assert_frame_equal(df, tables[0].df) + + +def test_stream_flag_size(): + df = pd.DataFrame(data_stream_flag_size) + + filename = os.path.join(testdir, "superscript.pdf") + tables = camelot.read_pdf(filename, flavor="stream", flag_size=True) + assert_frame_equal(df, tables[0].df) + + +def test_stream_strip_text(): + df = pd.DataFrame(data_stream_strip_text) + + filename = os.path.join(testdir, "detect_vertical_false.pdf") + tables = camelot.read_pdf(filename, flavor="stream", strip_text=" ,\n") + assert_frame_equal(df, tables[0].df) + + +def test_stream_edge_tol(): + df = pd.DataFrame(data_stream_edge_tol) + + filename = os.path.join(testdir, "edge_tol.pdf") + tables = camelot.read_pdf(filename, flavor="stream", edge_tol=500) + assert_frame_equal(df, tables[0].df) + + +def test_stream_layout_kwargs(): + df = pd.DataFrame(data_stream_layout_kwargs) + + filename = os.path.join(testdir, "detect_vertical_false.pdf") + tables = camelot.read_pdf( + filename, flavor="stream", layout_kwargs={"detect_vertical": False} + ) + assert_frame_equal(df, tables[0].df) + + +def test_stream_duplicated_text(): + df = pd.DataFrame(data_stream_duplicated_text) + + filename = os.path.join(testdir, "birdisland.pdf") + tables = camelot.read_pdf(filename, flavor="stream") + assert_frame_equal(df, tables[0].df) From 4c32c45534313f4cd81fb47e9375793b91e19ae5 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 05:33:23 +0530 Subject: [PATCH 12/22] Reorder tests --- tests/test_plotting.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index cec9df6..6283e9e 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -19,10 +19,10 @@ def test_text_plot(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) -def test_grid_plot(): - filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename) - return camelot.plot(tables[0], kind="grid") +def test_textedge_plot(): + filename = os.path.join(testdir, "tabula/12s0324.pdf") + tables = camelot.read_pdf(filename, flavor="stream") + return camelot.plot(tables[0], kind="textedge") @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) @@ -54,7 +54,7 @@ def test_joint_plot(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) -def test_textedge_plot(): - filename = os.path.join(testdir, "tabula/12s0324.pdf") - tables = camelot.read_pdf(filename, flavor="stream") - return camelot.plot(tables[0], kind="textedge") +def test_grid_plot(): + filename = os.path.join(testdir, "foo.pdf") + tables = camelot.read_pdf(filename) + return camelot.plot(tables[0], kind="grid") From 4dd1e7fb15f768adf3c872197885cd93d915483d Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 18:52:38 +0530 Subject: [PATCH 13/22] Call pdftopng in subprocess --- camelot/backends/ghostscript_backend.py | 4 +-- camelot/backends/poppler_backend.py | 11 ++++-- camelot/parsers/lattice.py | 1 - tests/test_common.py | 48 +++++++++---------------- 4 files changed, 27 insertions(+), 37 deletions(-) diff --git a/camelot/backends/ghostscript_backend.py b/camelot/backends/ghostscript_backend.py index cba3e83..e64510d 100644 --- a/camelot/backends/ghostscript_backend.py +++ b/camelot/backends/ghostscript_backend.py @@ -35,7 +35,7 @@ class GhostscriptBackend(object): "here: https://camelot-py.readthedocs.io/en/master/user/install-deps.html" ) - gs_args = [ + gs_command = [ "gs", "-q", "-sDEVICE=png16m", @@ -44,4 +44,4 @@ class GhostscriptBackend(object): f"-r{resolution}", pdf_path, ] - ghostscript.Ghostscript(*gs_args) + ghostscript.Ghostscript(*gs_command) diff --git a/camelot/backends/poppler_backend.py b/camelot/backends/poppler_backend.py index c806098..76c4667 100644 --- a/camelot/backends/poppler_backend.py +++ b/camelot/backends/poppler_backend.py @@ -1,8 +1,15 @@ # -*- coding: utf-8 -*- -from pdftopng import pdftopng +import subprocess class PopplerBackend(object): def convert(self, pdf_path, png_path): - pdftopng.convert(pdf_path, png_path) + pdftopng_command = ["pdftopng", pdf_path, png_path] + + try: + subprocess.check_output( + " ".join(pdftopng_command), stderr=subprocess.STDOUT, shell=True + ) + except subprocess.CalledProcessError as e: + raise ValueError(e.output) diff --git a/camelot/parsers/lattice.py b/camelot/parsers/lattice.py index 937e867..02ef794 100644 --- a/camelot/parsers/lattice.py +++ b/camelot/parsers/lattice.py @@ -6,7 +6,6 @@ import copy import locale import logging import warnings -import subprocess import numpy as np import pandas as pd diff --git a/tests/test_common.py b/tests/test_common.py index ddb8de2..88055d6 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -55,19 +55,17 @@ def test_repr_poppler(): tables = camelot.read_pdf(filename) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" def test_repr_ghostscript(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend(backend="ghostscript")) + tables = camelot.read_pdf( + filename, backend=ImageConversionBackend(backend="ghostscript") + ) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" def test_url_poppler(): @@ -75,19 +73,17 @@ def test_url_poppler(): tables = camelot.read_pdf(url) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" def test_url_ghostscript(): url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" - tables = camelot.read_pdf(url, backend=ImageConversionBackend(backend="ghostscript")) + tables = camelot.read_pdf( + url, backend=ImageConversionBackend(backend="ghostscript") + ) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" def test_pages_poppler(): @@ -95,23 +91,17 @@ def test_pages_poppler(): tables = camelot.read_pdf(url) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" tables = camelot.read_pdf(url, pages="1-end") assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" tables = camelot.read_pdf(url, pages="all") assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" def test_pages_ghostscript(): @@ -119,23 +109,17 @@ def test_pages_ghostscript(): tables = camelot.read_pdf(url) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" tables = camelot.read_pdf(url, pages="1-end") assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" tables = camelot.read_pdf(url, pages="all") assert repr(tables) == "" assert repr(tables[0]) == "
" - assert ( - repr(tables[0].cells[0][0]) == "" - ) + assert repr(tables[0].cells[0][0]) == "" def test_table_order(): From ff7260a228f4e5f5173058024da3be8688fd7189 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 19:02:26 +0530 Subject: [PATCH 14/22] Add separate tests for poppler and ghostscript --- ...lot.png => test_grid_plot_ghostscript.png} | Bin .../baseline_plots/test_grid_plot_poppler.png | Bin 0 -> 8339 bytes ...ot.png => test_joint_plot_ghostscript.png} | Bin .../test_joint_plot_poppler.png | Bin 0 -> 67202 bytes ...test_lattice_contour_plot_ghostscript.png} | Bin .../test_lattice_contour_plot_poppler.png | Bin 0 -> 65755 bytes ...lot.png => test_line_plot_ghostscript.png} | Bin .../baseline_plots/test_line_plot_poppler.png | Bin 0 -> 6784 bytes tests/test_plotting.py | 37 ++++++++++++++++-- 9 files changed, 33 insertions(+), 4 deletions(-) rename tests/files/baseline_plots/{test_grid_plot.png => test_grid_plot_ghostscript.png} (100%) create mode 100644 tests/files/baseline_plots/test_grid_plot_poppler.png rename tests/files/baseline_plots/{test_joint_plot.png => test_joint_plot_ghostscript.png} (100%) create mode 100644 tests/files/baseline_plots/test_joint_plot_poppler.png rename tests/files/baseline_plots/{test_lattice_contour_plot.png => test_lattice_contour_plot_ghostscript.png} (100%) create mode 100644 tests/files/baseline_plots/test_lattice_contour_plot_poppler.png rename tests/files/baseline_plots/{test_line_plot.png => test_line_plot_ghostscript.png} (100%) create mode 100644 tests/files/baseline_plots/test_line_plot_poppler.png diff --git a/tests/files/baseline_plots/test_grid_plot.png b/tests/files/baseline_plots/test_grid_plot_ghostscript.png similarity index 100% rename from tests/files/baseline_plots/test_grid_plot.png rename to tests/files/baseline_plots/test_grid_plot_ghostscript.png diff --git a/tests/files/baseline_plots/test_grid_plot_poppler.png b/tests/files/baseline_plots/test_grid_plot_poppler.png new file mode 100644 index 0000000000000000000000000000000000000000..6cf5a3907703d3601e0559771749568bdd0f38e2 GIT binary patch literal 8339 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 Zc$g;X{aWql)<)rLhvqIuqWY0b{|kf3gtq_y literal 0 HcmV?d00001 diff --git a/tests/files/baseline_plots/test_lattice_contour_plot.png b/tests/files/baseline_plots/test_lattice_contour_plot_ghostscript.png similarity index 100% rename from tests/files/baseline_plots/test_lattice_contour_plot.png rename to tests/files/baseline_plots/test_lattice_contour_plot_ghostscript.png diff --git a/tests/files/baseline_plots/test_lattice_contour_plot_poppler.png b/tests/files/baseline_plots/test_lattice_contour_plot_poppler.png new file mode 100644 index 0000000000000000000000000000000000000000..ec6eeb289929489996f82f15d8061ecc044665ba GIT binary patch literal 65755 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 literal 0 HcmV?d00001 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 6283e9e..39525d0 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -5,6 +5,7 @@ import os import pytest import camelot +from camelot.backends.image_conversion import ImageConversionBackend testdir = os.path.dirname(os.path.abspath(__file__)) @@ -26,12 +27,19 @@ def test_textedge_plot(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) -def test_lattice_contour_plot(): +def test_lattice_contour_plot_poppler(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) return camelot.plot(tables[0], kind="contour") +@pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) +def test_lattice_contour_plot_ghostscript(): + filename = os.path.join(testdir, "foo.pdf") + tables = camelot.read_pdf(filename, backend=ImageConversionBackend('ghostscript')) + return camelot.plot(tables[0], kind="contour") + + @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_stream_contour_plot(): filename = os.path.join(testdir, "tabula/12s0324.pdf") @@ -40,21 +48,42 @@ def test_stream_contour_plot(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) -def test_line_plot(): +def test_line_plot_poppler(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) return camelot.plot(tables[0], kind="line") @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) -def test_joint_plot(): +def test_line_plot_ghostscript(): + filename = os.path.join(testdir, "foo.pdf") + tables = camelot.read_pdf(filename, backend=ImageConversionBackend('ghostscript')) + return camelot.plot(tables[0], kind="line") + + +@pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) +def test_joint_plot_poppler(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) return camelot.plot(tables[0], kind="joint") @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) -def test_grid_plot(): +def test_joint_plot_ghostscript(): + filename = os.path.join(testdir, "foo.pdf") + tables = camelot.read_pdf(filename, backend=ImageConversionBackend('ghostscript')) + return camelot.plot(tables[0], kind="joint") + + +@pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) +def test_grid_plot_poppler(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename) return camelot.plot(tables[0], kind="grid") + + +@pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) +def test_grid_plot_ghostscript(): + filename = os.path.join(testdir, "foo.pdf") + tables = camelot.read_pdf(filename, backend=ImageConversionBackend('ghostscript')) + return camelot.plot(tables[0], kind="grid") From 0c20cb0be8d4626ead6641c14153c60dd5170a9e Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 19:21:50 +0530 Subject: [PATCH 15/22] Update gh workflow and remove logging --- .github/workflows/tests.yml | 23 ++++++++++++++++++++++- camelot/backends/image_conversion.py | 18 ++++++++---------- tests/test_image_conversion_backend.py | 0 tests/test_plotting.py | 8 ++++---- 4 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 tests/test_image_conversion_backend.py diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 09254e6..04a7cf8 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -7,7 +7,28 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: [3.6, 3.7, 3.8, 3.9] + python-version: [3.6, 3.7, 3.8] + + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install camelot with dependencies + run: | + make install + - name: Test with pytest + run: | + make test + + test_latest: + name: Test on ${{ matrix.os }} with Python 3.9 + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: [3.9] steps: - uses: actions/checkout@v2 diff --git a/camelot/backends/image_conversion.py b/camelot/backends/image_conversion.py index 004bce7..f79c236 100644 --- a/camelot/backends/image_conversion.py +++ b/camelot/backends/image_conversion.py @@ -1,11 +1,8 @@ # -*- coding: utf-8 -*- -import logging - from .poppler_backend import PopplerBackend from .ghostscript_backend import GhostscriptBackend -logger = logging.getLogger("camelot") backends = {"poppler": PopplerBackend, "ghostscript": GhostscriptBackend} @@ -23,9 +20,7 @@ class ImageConversionBackend(object): converter = backends[self.backend]() converter.convert(pdf_path, png_path) except Exception as e: - logger.info( - f"Image conversion backend '{self.backend}' failed with '{str(e)}'" - ) + import sys if self.use_fallback: for fallback in self.fallbacks: @@ -35,10 +30,13 @@ class ImageConversionBackend(object): converter = backends[fallback]() converter.convert(pdf_path, png_path) except Exception as e: - logger.info( - f"Image conversion backend '{fallback}' failed with '{str(e)}'" - ) + raise type(e)( + str(e) + f" with image conversion backend '{fallback}'" + ).with_traceback(sys.exc_info()[2]) continue else: - logger.info(f"Image conversion backend '{fallback}' succeeded") break + else: + raise type(e)( + str(e) + f" with image conversion backend '{self.backend}'" + ).with_traceback(sys.exc_info()[2]) diff --git a/tests/test_image_conversion_backend.py b/tests/test_image_conversion_backend.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 39525d0..5573c34 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -36,7 +36,7 @@ def test_lattice_contour_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_lattice_contour_plot_ghostscript(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend('ghostscript')) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) return camelot.plot(tables[0], kind="contour") @@ -57,7 +57,7 @@ def test_line_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_line_plot_ghostscript(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend('ghostscript')) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) return camelot.plot(tables[0], kind="line") @@ -71,7 +71,7 @@ def test_joint_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_joint_plot_ghostscript(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend('ghostscript')) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) return camelot.plot(tables[0], kind="joint") @@ -85,5 +85,5 @@ def test_grid_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_grid_plot_ghostscript(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend('ghostscript')) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) return camelot.plot(tables[0], kind="grid") From e76a7a7c2667112de0bc5127c98ddc1f187700cc Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 19:27:29 +0530 Subject: [PATCH 16/22] Delay ghostscript import --- camelot/backends/ghostscript_backend.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/camelot/backends/ghostscript_backend.py b/camelot/backends/ghostscript_backend.py index e64510d..5e93cdb 100644 --- a/camelot/backends/ghostscript_backend.py +++ b/camelot/backends/ghostscript_backend.py @@ -4,8 +4,6 @@ import sys import ctypes from ctypes.util import find_library -import ghostscript - def installed_posix(): library = find_library("gs") @@ -35,6 +33,8 @@ class GhostscriptBackend(object): "here: https://camelot-py.readthedocs.io/en/master/user/install-deps.html" ) + import ghostscript + gs_command = [ "gs", "-q", From 4c78dadd55764bf64f99b368f2e19e02d33206bf Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 19:40:53 +0530 Subject: [PATCH 17/22] Skip ghostscript tests on windows --- setup.py | 1 + tests/test_common.py | 10 ++++++++++ tests/test_plotting.py | 13 +++++++++++++ 3 files changed, 24 insertions(+) diff --git a/setup.py b/setup.py index 5e99ea8..98bbc5d 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ def setup_package(): extras_require={ "all": all_requires, "base": base_requires, + "cv": base_requires, # deprecate in v0.12.0 "dev": dev_requires, "plot": plot_requires, }, diff --git a/tests/test_common.py b/tests/test_common.py index 88055d6..4ffd302 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import sys import pandas as pd from pandas.testing import assert_frame_equal @@ -59,6 +60,9 @@ def test_repr_poppler(): def test_repr_ghostscript(): + if sys.platform not in ["linux", "darwin"]: + return True + filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf( filename, backend=ImageConversionBackend(backend="ghostscript") @@ -77,6 +81,9 @@ def test_url_poppler(): def test_url_ghostscript(): + if sys.platform not in ["linux", "darwin"]: + return True + url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" tables = camelot.read_pdf( url, backend=ImageConversionBackend(backend="ghostscript") @@ -105,6 +112,9 @@ def test_pages_poppler(): def test_pages_ghostscript(): + if sys.platform not in ["linux", "darwin"]: + return True + url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" tables = camelot.read_pdf(url) assert repr(tables) == "" diff --git a/tests/test_plotting.py b/tests/test_plotting.py index 5573c34..d53e3bc 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import os +import sys import pytest @@ -35,6 +36,9 @@ def test_lattice_contour_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_lattice_contour_plot_ghostscript(): + if sys.platform not in ["linux", "darwin"]: + return True + filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) return camelot.plot(tables[0], kind="contour") @@ -56,6 +60,9 @@ def test_line_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_line_plot_ghostscript(): + if sys.platform not in ["linux", "darwin"]: + return True + filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) return camelot.plot(tables[0], kind="line") @@ -70,6 +77,9 @@ def test_joint_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_joint_plot_ghostscript(): + if sys.platform not in ["linux", "darwin"]: + return True + filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) return camelot.plot(tables[0], kind="joint") @@ -84,6 +94,9 @@ def test_grid_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_grid_plot_ghostscript(): + if sys.platform not in ["linux", "darwin"]: + return True + filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) return camelot.plot(tables[0], kind="grid") From 4628be925108631dc03eb9d4a0f8923cf1a501cf Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 19:51:01 +0530 Subject: [PATCH 18/22] Skip ghostscript tests on windows --- tests/test_plotting.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_plotting.py b/tests/test_plotting.py index d53e3bc..e13fc98 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -37,7 +37,7 @@ def test_lattice_contour_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_lattice_contour_plot_ghostscript(): if sys.platform not in ["linux", "darwin"]: - return True + pytest.skip("Skipping ghostscript test on Windows") filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) @@ -61,7 +61,7 @@ def test_line_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_line_plot_ghostscript(): if sys.platform not in ["linux", "darwin"]: - return True + pytest.skip("Skipping ghostscript test on Windows") filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) @@ -78,7 +78,7 @@ def test_joint_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_joint_plot_ghostscript(): if sys.platform not in ["linux", "darwin"]: - return True + pytest.skip("Skipping ghostscript test on Windows") filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) @@ -95,7 +95,7 @@ def test_grid_plot_poppler(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_grid_plot_ghostscript(): if sys.platform not in ["linux", "darwin"]: - return True + pytest.skip("Skipping ghostscript test on Windows") filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) From c3b0fa30dcfee25d0c05ee0b23cac81b746aa371 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 20:02:47 +0530 Subject: [PATCH 19/22] Remove logger.info and use shutil.which to find pdftopng executable --- camelot/backends/image_conversion.py | 2 -- camelot/backends/poppler_backend.py | 9 ++++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/camelot/backends/image_conversion.py b/camelot/backends/image_conversion.py index f79c236..a9b6004 100644 --- a/camelot/backends/image_conversion.py +++ b/camelot/backends/image_conversion.py @@ -24,8 +24,6 @@ class ImageConversionBackend(object): if self.use_fallback: for fallback in self.fallbacks: - logger.info(f"Falling back on '{fallback}'") - try: converter = backends[fallback]() converter.convert(pdf_path, png_path) diff --git a/camelot/backends/poppler_backend.py b/camelot/backends/poppler_backend.py index 76c4667..ab12bcf 100644 --- a/camelot/backends/poppler_backend.py +++ b/camelot/backends/poppler_backend.py @@ -1,11 +1,18 @@ # -*- coding: utf-8 -*- +import shutil import subprocess class PopplerBackend(object): def convert(self, pdf_path, png_path): - pdftopng_command = ["pdftopng", pdf_path, png_path] + pdftopng_executable = shutil.which("pdftopng") + if pdftopng_executable is None: + raise OSError( + "pdftopng is not installed. Please install it using the `pip install pdftopng` command." + ) + + pdftopng_command = [pdftopng_executable, pdf_path, png_path] try: subprocess.check_output( From 458181cd1dbdf64a0f1af0ea005d2f302560447b Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Sun, 4 Jul 2021 20:22:32 +0530 Subject: [PATCH 20/22] Don't use fallback for image conversion backend tests --- tests/test_common.py | 51 +++++++++++++++++++++++++++++++----------- tests/test_plotting.py | 16 ++++++------- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/tests/test_common.py b/tests/test_common.py index 4ffd302..86889d5 100644 --- a/tests/test_common.py +++ b/tests/test_common.py @@ -53,7 +53,9 @@ def test_password(): def test_repr_poppler(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename) + tables = camelot.read_pdf( + filename, backend=ImageConversionBackend(backend="poppler", use_fallback=False) + ) assert repr(tables) == "" assert repr(tables[0]) == "
" assert repr(tables[0].cells[0][0]) == "" @@ -65,7 +67,8 @@ def test_repr_ghostscript(): filename = os.path.join(testdir, "foo.pdf") tables = camelot.read_pdf( - filename, backend=ImageConversionBackend(backend="ghostscript") + filename, + backend=ImageConversionBackend(backend="ghostscript", use_fallback=False), ) assert repr(tables) == "" assert repr(tables[0]) == "
" @@ -74,7 +77,9 @@ def test_repr_ghostscript(): def test_url_poppler(): url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" - tables = camelot.read_pdf(url) + tables = camelot.read_pdf( + url, backend=ImageConversionBackend(backend="poppler", use_fallback=False) + ) assert repr(tables) == "" assert repr(tables[0]) == "
" assert repr(tables[0].cells[0][0]) == "" @@ -86,7 +91,7 @@ def test_url_ghostscript(): url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" tables = camelot.read_pdf( - url, backend=ImageConversionBackend(backend="ghostscript") + url, backend=ImageConversionBackend(backend="ghostscript", use_fallback=False) ) assert repr(tables) == "" assert repr(tables[0]) == "
" @@ -95,17 +100,27 @@ def test_url_ghostscript(): def test_pages_poppler(): url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" - tables = camelot.read_pdf(url) + tables = camelot.read_pdf( + url, backend=ImageConversionBackend(backend="poppler", use_fallback=False) + ) assert repr(tables) == "" assert repr(tables[0]) == "
" assert repr(tables[0].cells[0][0]) == "" - tables = camelot.read_pdf(url, pages="1-end") + tables = camelot.read_pdf( + url, + pages="1-end", + backend=ImageConversionBackend(backend="poppler", use_fallback=False), + ) assert repr(tables) == "" assert repr(tables[0]) == "
" assert repr(tables[0].cells[0][0]) == "" - tables = camelot.read_pdf(url, pages="all") + tables = camelot.read_pdf( + url, + pages="all", + backend=ImageConversionBackend(backend="poppler", use_fallback=False), + ) assert repr(tables) == "" assert repr(tables[0]) == "
" assert repr(tables[0].cells[0][0]) == "" @@ -116,20 +131,30 @@ def test_pages_ghostscript(): return True url = "https://camelot-py.readthedocs.io/en/master/_static/pdf/foo.pdf" - tables = camelot.read_pdf(url) + tables = camelot.read_pdf( + url, backend=ImageConversionBackend(backend="ghostscript", use_fallback=False) + ) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert repr(tables[0].cells[0][0]) == "" + assert repr(tables[0].cells[0][0]) == "" - tables = camelot.read_pdf(url, pages="1-end") + tables = camelot.read_pdf( + url, + pages="1-end", + backend=ImageConversionBackend(backend="ghostscript", use_fallback=False), + ) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert repr(tables[0].cells[0][0]) == "" + assert repr(tables[0].cells[0][0]) == "" - tables = camelot.read_pdf(url, pages="all") + tables = camelot.read_pdf( + url, + pages="all", + backend=ImageConversionBackend(backend="ghostscript", use_fallback=False), + ) assert repr(tables) == "" assert repr(tables[0]) == "
" - assert repr(tables[0].cells[0][0]) == "" + assert repr(tables[0].cells[0][0]) == "" def test_table_order(): diff --git a/tests/test_plotting.py b/tests/test_plotting.py index e13fc98..5be7a48 100644 --- a/tests/test_plotting.py +++ b/tests/test_plotting.py @@ -30,7 +30,7 @@ def test_textedge_plot(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_lattice_contour_plot_poppler(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("poppler", use_fallback=False)) return camelot.plot(tables[0], kind="contour") @@ -40,7 +40,7 @@ def test_lattice_contour_plot_ghostscript(): pytest.skip("Skipping ghostscript test on Windows") filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript", use_fallback=False)) return camelot.plot(tables[0], kind="contour") @@ -54,7 +54,7 @@ def test_stream_contour_plot(): @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_line_plot_poppler(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("poppler", use_fallback=False)) return camelot.plot(tables[0], kind="line") @@ -64,14 +64,14 @@ def test_line_plot_ghostscript(): pytest.skip("Skipping ghostscript test on Windows") filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript", use_fallback=False)) return camelot.plot(tables[0], kind="line") @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_joint_plot_poppler(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("poppler", use_fallback=False)) return camelot.plot(tables[0], kind="joint") @@ -81,14 +81,14 @@ def test_joint_plot_ghostscript(): pytest.skip("Skipping ghostscript test on Windows") filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript", use_fallback=False)) return camelot.plot(tables[0], kind="joint") @pytest.mark.mpl_image_compare(baseline_dir="files/baseline_plots", remove_text=True) def test_grid_plot_poppler(): filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("poppler", use_fallback=False)) return camelot.plot(tables[0], kind="grid") @@ -98,5 +98,5 @@ def test_grid_plot_ghostscript(): pytest.skip("Skipping ghostscript test on Windows") filename = os.path.join(testdir, "foo.pdf") - tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript")) + tables = camelot.read_pdf(filename, backend=ImageConversionBackend("ghostscript", use_fallback=False)) return camelot.plot(tables[0], kind="grid") From 65b4ea623cfc0da62448c290c8e14faffc272b40 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 5 Jul 2021 04:35:14 +0530 Subject: [PATCH 21/22] Update pdftopng version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 98bbc5d..371d6cd 100644 --- a/setup.py +++ b/setup.py @@ -24,7 +24,7 @@ requires = [ "tabulate>=0.8.9", ] -base_requires = ["ghostscript>=0.7", "opencv-python>=3.4.2.17", "pdftopng>=0.1.1"] +base_requires = ["ghostscript>=0.7", "opencv-python>=3.4.2.17", "pdftopng>=0.2.3"] plot_requires = [ "matplotlib>=2.2.3", From e9c0f556900f1b8021350c27464b7a1c93975270 Mon Sep 17 00:00:00 2001 From: Vinayak Mehta Date: Mon, 5 Jul 2021 05:12:43 +0530 Subject: [PATCH 22/22] Test image_conversion.py --- tests/test_image_conversion_backend.py | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/tests/test_image_conversion_backend.py b/tests/test_image_conversion_backend.py index e69de29..8074cac 100644 --- a/tests/test_image_conversion_backend.py +++ b/tests/test_image_conversion_backend.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- + +import pytest + +import camelot.backends.image_conversion +from camelot.backends import ImageConversionBackend +from camelot.backends.poppler_backend import PopplerBackend +from camelot.backends.ghostscript_backend import GhostscriptBackend + + +class PopplerBackendError(object): + def convert(self, pdf_path, png_path): + raise ValueError('conversion failed') + + +class GhostscriptBackendError(object): + def convert(self, pdf_path, png_path): + raise ValueError('conversion failed') + + +class GhostscriptBackendNoError(object): + def convert(self, pdf_path, png_path): + pass + + +def test_poppler_backend_error_when_no_use_fallback(monkeypatch): + backends = {"poppler": PopplerBackendError, "ghostscript": GhostscriptBackendNoError} + monkeypatch.setattr("camelot.backends.image_conversion.backends", backends, raising=True) + backend = ImageConversionBackend(use_fallback=False) + + message = "conversion failed with image conversion backend 'poppler'" + with pytest.raises(ValueError, match=message): + backend.convert('foo', 'bar') + +def test_ghostscript_backend_when_use_fallback(monkeypatch): + backends = {"poppler": PopplerBackendError, "ghostscript": GhostscriptBackendNoError} + monkeypatch.setattr("camelot.backends.image_conversion.backends", backends, raising=True) + backend = ImageConversionBackend() + backend.convert('foo', 'bar') + + +def test_ghostscript_backend_error_when_use_fallback(monkeypatch): + backends = {"poppler": PopplerBackendError, "ghostscript": GhostscriptBackendError} + monkeypatch.setattr("camelot.backends.image_conversion.backends", backends, raising=True) + backend = ImageConversionBackend() + + message = "conversion failed with image conversion backend 'ghostscript'" + with pytest.raises(ValueError, match=message): + backend.convert('foo', 'bar')