summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGES.rst79
-rw-r--r--MANIFEST.in1
-rw-r--r--PKG-INFO554
-rw-r--r--README.md139
-rw-r--r--debian/changelog121
-rw-r--r--debian/compat1
-rw-r--r--debian/control22
-rw-r--r--debian/copyright4
-rw-r--r--debian/img2pdf.install2
-rw-r--r--debian/not-installed1
-rw-r--r--debian/patches/disable-gui.patch27
-rw-r--r--debian/patches/imagemagick-issue-28525
-rw-r--r--debian/patches/remove-exact-cmyk8.patch85
-rw-r--r--debian/patches/series3
-rw-r--r--debian/pybuild.testfiles2
-rwxr-xr-xdebian/rules19
-rw-r--r--debian/salsa-ci.yml8
-rw-r--r--debian/source/options1
-rw-r--r--debian/tests/control22
-rw-r--r--debian/tests/default12
-rwxr-xr-xdebian/tests/pytest3
-rwxr-xr-xdebian/tests/test-sh3
-rw-r--r--debian/upstream/metadata7
-rw-r--r--debian/upstream/signing-key.asc110
-rw-r--r--debian/watch4
-rw-r--r--setup.cfg3
-rw-r--r--setup.py73
-rw-r--r--src/img2pdf.egg-info/PKG-INFO554
-rw-r--r--src/img2pdf.egg-info/SOURCES.txt5
-rw-r--r--src/img2pdf.egg-info/entry_points.txt10
-rw-r--r--src/img2pdf.egg-info/pbr.json1
-rw-r--r--src/img2pdf.egg-info/requires.txt5
-rwxr-xr-xsrc/img2pdf.py2578
-rwxr-xr-xsrc/img2pdf_test.py7159
-rw-r--r--src/jp2.py92
-rw-r--r--src/tests/__init__.py732
-rw-r--r--src/tests/input/animation.gifbin1930 -> 1962 bytes
-rw-r--r--src/tests/output/animation.gif.pdfbin6070 -> 6101 bytes
-rwxr-xr-xtest.sh1468
39 files changed, 10648 insertions, 3287 deletions
diff --git a/CHANGES.rst b/CHANGES.rst
index a9ab56b..df714cf 100644
--- a/CHANGES.rst
+++ b/CHANGES.rst
@@ -2,6 +2,85 @@
CHANGES
=======
+0.5.1 (2023-11-26)
+------------------
+
+ - no default ICC profile location for PDF/A-1b on Windows
+ - workaround for PNG input without dpi units but non-square dpi aspect ratio
+
+0.5.0 (2023-10-28)
+------------------
+
+ - support MIFF for 16 bit CMYK input
+ - accept pathlib.Path objects as input
+ - don't store RGB ICC profiles from bilevel or grayscale TIFF, PNG and JPEG
+ - thumbnails are no longer included by default and --include-thumbnails has to
+ be used if you want them
+ - support for pikepdf (>= 6.2.0)
+
+0.4.4 (2022-04-07)
+------------------
+
+ - --viewer-page-layout support for twopageright and twopageleft
+ - Add B and JB paper sizes
+ - support for pikepdf (>= 5.0.0) and Pillow (>= 9.1.0)
+
+0.4.3 (2021-10-24)
+------------------
+
+ - fix --viewer-initial-page (broken in last release)
+
+0.4.2 (2021-10-11)
+------------------
+
+ - add --rotation
+ - allow palette PNG images with ICC profile
+ - sort globbing result on windows
+ - convert 8-bit PNG alpha channels to /SMasks in PDF
+ - remove pdfrw from tests
+
+0.4.1 (2021-05-09)
+------------------
+
+ - support wildcards in paths on windows
+ - support MPO images
+ - fix page border computation
+ - use "img2pdf" logger instead of "root" logger
+ - add --from-file
+
+0.4.0 (2020-08-07)
+------------------
+
+ - replace --without-pdfrw by --engine=internal or --engine=pdfrw
+ - add pikepdf as additional rendering engine and add --engine=pikepdf
+ - support for creating PDF/A-1b compliant PDF using the --pdfa option
+ (this also requires the presence of an ICC profile somewhere on the system)
+ - support for images with embedded ICC profile as input
+ - rewrite tests
+ * use pytest via tox
+ * use pikepdf instead of pdfrw
+ * use imagemagick json output instead of identify -verbose
+ - format all code with black
+
+0.3.6 (2020-04-30)
+------------------
+
+ - fix tests for Fedora on arm64
+
+0.3.5 (2020-04-28)
+------------------
+
+ - remove all Python 2 support
+ - disable pdfrw by default
+
+0.3.4 (2020-04-05)
+------------------
+
+ - test.sh: replace imagemagick with custom python script to produce bit-by-bit
+ identical results on all architectures
+ - add --crop-border, --bleed-border, --trim-border and --art-border options
+ - first draft of a rudimentary tkinter gui (run with --gui)
+
0.3.3 (2019-01-07)
------------------
diff --git a/MANIFEST.in b/MANIFEST.in
index d86af25..a217cc3 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1,6 +1,7 @@
include README.md
include test_comp.sh
include test.sh
+include magick.py
include CHANGES.rst
include LICENSE
recursive-include src *.jpg
diff --git a/PKG-INFO b/PKG-INFO
index 7553591..cc05494 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,240 +1,18 @@
Metadata-Version: 2.1
Name: img2pdf
-Version: 0.3.3
+Version: 0.5.1
Summary: Convert images to PDF via direct JPEG inclusion.
Home-page: https://gitlab.mister-muffin.de/josch/img2pdf
-Author: Johannes 'josch' Schauer
+Download-URL: https://gitlab.mister-muffin.de/josch/img2pdf/repository/archive.tar.gz?ref=0.5.1
+Author: Johannes Schauer Marin Rodrigues
Author-email: josch@mister-muffin.de
License: LGPL
-Download-URL: https://gitlab.mister-muffin.de/josch/img2pdf/repository/archive.tar.gz?ref=0.3.3
-Description: img2pdf
- =======
-
- Lossless conversion of raster images to PDF. You should use img2pdf if your
- priorities are (in this order):
-
- 1. **always lossless**: the image embedded in the PDF will always have the
- exact same color information for every pixel as the input
- 2. **small**: if possible, the difference in filesize between the input image
- and the output PDF will only be the overhead of the PDF container itself
- 3. **fast**: if possible, the input image is just pasted into the PDF document
- as-is without any CPU hungry re-encoding of the pixel data
-
- Conventional conversion software (like ImageMagick) would either:
-
- 1. not be lossless because lossy re-encoding to JPEG
- 2. not be small because using wasteful flate encoding of raw pixel data
- 3. not be fast because input data gets re-encoded
-
- Another advantage of not having to re-encode the input (in most common
- situations) is, that img2pdf is able to handle much larger input than other
- software, because the raw pixel data never has to be loaded into memory.
-
- The following table shows how img2pdf handles different input depending on the
- input file format and image color space.
-
- | Format | Colorspace | Result |
- | -------------------- | ------------------------------ | ------------- |
- | JPEG | any | direct |
- | JPEG2000 | any | direct |
- | PNG (non-interlaced) | any | direct |
- | TIFF (CCITT Group 4) | monochrome | direct |
- | any | any except CMYK and monochrome | PNG Paeth |
- | any | monochrome | CCITT Group 4 |
- | any | CMYK | flate |
-
- For JPEG, JPEG2000, non-interlaced PNG and TIFF images with CCITT Group 4
- encoded data, img2pdf directly embeds the image data into the PDF without
- re-encoding it. It thus treats the PDF format merely as a container format for
- the image data. In these cases, img2pdf only increases the filesize by the size
- of the PDF container (typically around 500 to 700 bytes). Since data is only
- copied and not re-encoded, img2pdf is also typically faster than other
- solutions for these input formats.
-
- For all other input types, img2pdf first has to transform the pixel data to
- make it compatible with PDF. In most cases, the PNG Paeth filter is applied to
- the pixel data. For monochrome input, CCITT Group 4 is used instead. Only for
- CMYK input no filter is applied before finally applying flate compression.
-
- Usage
- -----
-
- The images must be provided as files because img2pdf needs to seek in the file
- descriptor.
-
- If no output file is specified with the `-o`/`--output` option, output will be
- done to stdout. A typical invocation is:
-
- $ img2pdf img1.png img2.jpg -o out.pdf
-
- The detailed documentation can be accessed by running:
-
- $ img2pdf --help
-
- Bugs
- ----
-
- - If you find a JPEG, JPEG2000, PNG or CCITT Group 4 encoded TIFF file that,
- when embedded into the PDF cannot be read by the Adobe Acrobat Reader,
- please contact me.
-
- - I have not yet figured out how to determine the colorspace of JPEG2000
- files. Therefore JPEG2000 files use DeviceRGB by default. For JPEG2000
- files with other colorspaces, you must explicitly specify it using the
- `--colorspace` option.
-
- - Input images with alpha channels are not allowed. PDF doesn't support alpha
- channels in images and thus, the alpha channel of the input would have to be
- discarded. But img2pdf will always be lossless and thus, input images must
- not carry transparency information.
-
- - img2pdf uses PIL (or Pillow) to obtain image meta data and to convert the
- input if necessary. To prevent decompression bomb denial of service attacks,
- Pillow limits the maximum number of pixels an input image is allowed to
- have. If you are sure that you know what you are doing, then you can disable
- this safeguard by passing the `--pillow-limit-break` option to img2pdf. This
- allows one to process even very large input images.
-
- Installation
- ------------
-
- On a Debian- and Ubuntu-based systems, img2pdf can be installed from the
- official repositories:
-
- $ apt install img2pdf
-
- If you want to install it using pip, you can run:
-
- $ pip3 install img2pdf
-
- If you prefer to install from source code use:
-
- $ cd img2pdf/
- $ pip3 install .
-
- To test the console script without installing the package on your system,
- use virtualenv:
-
- $ cd img2pdf/
- $ virtualenv ve
- $ ve/bin/pip3 install .
-
- You can then test the converter using:
-
- $ ve/bin/img2pdf -o test.pdf src/tests/test.jpg
-
- The package can also be used as a library:
-
- import img2pdf
-
- # opening from filename
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert('test.jpg'))
-
- # opening from file handle
- with open("name.pdf","wb") as f1, open("test.jpg") as f2:
- f1.write(img2pdf.convert(f2))
-
- # using in-memory image data
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert("\x89PNG...")
-
- # multiple inputs (variant 1)
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert("test1.jpg", "test2.png"))
-
- # multiple inputs (variant 2)
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert(["test1.jpg", "test2.png"]))
-
- # writing to file descriptor
- with open("name.pdf","wb") as f1, open("test.jpg") as f2:
- img2pdf.convert(f2, outputstream=f1)
-
- # specify paper size (A4)
- a4inpt = (img2pdf.mm_to_pt(210),img2pdf.mm_to_pt(297))
- layout_fun = img2pdf.get_layout_fun(a4inpt)
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert('test.jpg', layout_fun=layout_fun))
-
- Comparison to ImageMagick
- -------------------------
-
- Create a large test image:
-
- $ convert logo: -resize 8000x original.jpg
-
- Convert it into PDF using ImageMagick and img2pdf:
-
- $ time img2pdf original.jpg -o img2pdf.pdf
- $ time convert original.jpg imagemagick.pdf
-
- Notice how ImageMagick took an order of magnitude longer to do the conversion
- than img2pdf. It also used twice the memory.
-
- Now extract the image data from both PDF documents and compare it to the
- original:
-
- $ pdfimages -all img2pdf.pdf tmp
- $ compare -metric AE original.jpg tmp-000.jpg null:
- 0
- $ pdfimages -all imagemagick.pdf tmp
- $ compare -metric AE original.jpg tmp-000.jpg null:
- 118716
-
- To get lossless output with ImageMagick we can use Zip compression but that
- unnecessarily increases the size of the output:
-
- $ convert original.jpg -compress Zip imagemagick.pdf
- $ pdfimages -all imagemagick.pdf tmp
- $ compare -metric AE original.jpg tmp-000.png null:
- 0
- $ stat --format="%s %n" original.jpg img2pdf.pdf imagemagick.pdf
- 1535837 original.jpg
- 1536683 img2pdf.pdf
- 9397809 imagemagick.pdf
-
- Comparison to pdfLaTeX
- ----------------------
-
- pdfLaTeX performs a lossless conversion from included images to PDF by default.
- If the input is a JPEG, then it simply embeds the JPEG into the PDF in the same
- way as img2pdf does it. But for other image formats it uses flate compression
- of the plain pixel data and thus needlessly increases the output file size:
-
- $ convert logo: -resize 8000x original.png
- $ cat << END > pdflatex.tex
- \documentclass{article}
- \usepackage{graphicx}
- \begin{document}
- \includegraphics{original.png}
- \end{document}
- END
- $ pdflatex pdflatex.tex
- $ stat --format="%s %n" original.png pdflatex.pdf
- 4500182 original.png
- 9318120 pdflatex.pdf
-
- Comparison to Tesseract OCR
- ---------------------------
-
- Tesseract OCR comes closest to the functionality img2pdf provides. It is able
- to convert JPEG and PNG input to PDF without needlessly increasing the filesize
- and is at the same time lossless. So if your input is JPEG and PNG images, then
- you should safely be able to use Tesseract instead of img2pdf. For other input,
- Tesseract might not do a lossless conversion. For example it converts CMYK
- input to RGB and removes the alpha channel from images with transparency. For
- multipage TIFF or animated GIF, it will only convert the first frame.
-
Keywords: jpeg pdf converter
-Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Other Audience
Classifier: Environment :: Console
Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -242,4 +20,328 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
-Provides-Extra: test
+Description-Content-Type: text/markdown
+Provides-Extra: gui
+License-File: LICENSE
+
+[![Travis Status](https://travis-ci.com/josch/img2pdf.svg?branch=main)](https://app.travis-ci.com/josch/img2pdf)
+[![Appveyor Status](https://ci.appveyor.com/api/projects/status/2kws3wkqvi526llj/branch/main?svg=true)](https://ci.appveyor.com/project/josch/img2pdf/branch/main)
+
+img2pdf
+=======
+
+Lossless conversion of raster images to PDF. You should use img2pdf if your
+priorities are (in this order):
+
+ 1. **always lossless**: the image embedded in the PDF will always have the
+ exact same color information for every pixel as the input
+ 2. **small**: if possible, the difference in filesize between the input image
+ and the output PDF will only be the overhead of the PDF container itself
+ 3. **fast**: if possible, the input image is just pasted into the PDF document
+ as-is without any CPU hungry re-encoding of the pixel data
+
+Conventional conversion software (like ImageMagick) would either:
+
+ 1. not be lossless because lossy re-encoding to JPEG
+ 2. not be small because using wasteful flate encoding of raw pixel data
+ 3. not be fast because input data gets re-encoded
+
+Another advantage of not having to re-encode the input (in most common
+situations) is, that img2pdf is able to handle much larger input than other
+software, because the raw pixel data never has to be loaded into memory.
+
+The following table shows how img2pdf handles different input depending on the
+input file format and image color space.
+
+| Format | Colorspace | Result |
+| ------------------------------------- | ------------------------------ | ------------- |
+| JPEG | any | direct |
+| JPEG2000 | any | direct |
+| PNG (non-interlaced, no transparency) | any | direct |
+| TIFF (CCITT Group 4) | monochrome | direct |
+| any | any except CMYK and monochrome | PNG Paeth |
+| any | monochrome | CCITT Group 4 |
+| any | CMYK | flate |
+
+For JPEG, JPEG2000, non-interlaced PNG and TIFF images with CCITT Group 4
+encoded data, img2pdf directly embeds the image data into the PDF without
+re-encoding it. It thus treats the PDF format merely as a container format for
+the image data. In these cases, img2pdf only increases the filesize by the size
+of the PDF container (typically around 500 to 700 bytes). Since data is only
+copied and not re-encoded, img2pdf is also typically faster than other
+solutions for these input formats.
+
+For all other input types, img2pdf first has to transform the pixel data to
+make it compatible with PDF. In most cases, the PNG Paeth filter is applied to
+the pixel data. For monochrome input, CCITT Group 4 is used instead. Only for
+CMYK input no filter is applied before finally applying flate compression.
+
+Usage
+-----
+
+The images must be provided as files because img2pdf needs to seek in the file
+descriptor.
+
+If no output file is specified with the `-o`/`--output` option, output will be
+done to stdout. A typical invocation is:
+
+ $ img2pdf img1.png img2.jpg -o out.pdf
+
+The detailed documentation can be accessed by running:
+
+ $ img2pdf --help
+
+Bugs
+----
+
+ - If you find a JPEG, JPEG2000, PNG or CCITT Group 4 encoded TIFF file that,
+ when embedded into the PDF cannot be read by the Adobe Acrobat Reader,
+ please contact me.
+
+ - An error is produced if the input image is broken. This commonly happens if
+ the input image has an invalid EXIF Orientation value of zero. Even though
+ only nine different values from 1 to 9 are permitted, Anroid phones and
+ Canon DSLR cameras produce JPEG images with the invalid value of zero.
+ Either fix your input images with `exiftool` or similar software before
+ passing the JPEG to `img2pdf` or run `img2pdf` with `--rotation=ifvalid`
+ (if you run img2pdf from the commandline) or by passing
+ `rotation=img2pdf.Rotation.ifvalid` as an argument to `convert()` when using
+ img2pdf as a library.
+
+ - img2pdf uses PIL (or Pillow) to obtain image meta data and to convert the
+ input if necessary. To prevent decompression bomb denial of service attacks,
+ Pillow limits the maximum number of pixels an input image is allowed to
+ have. If you are sure that you know what you are doing, then you can disable
+ this safeguard by passing the `--pillow-limit-break` option to img2pdf. This
+ allows one to process even very large input images.
+
+Installation
+------------
+
+On a Debian- and Ubuntu-based systems, img2pdf can be installed from the
+official repositories:
+
+ $ apt install img2pdf
+
+If you want to install it using pip, you can run:
+
+ $ pip3 install img2pdf
+
+If you prefer to install from source code use:
+
+ $ cd img2pdf/
+ $ pip3 install .
+
+To test the console script without installing the package on your system,
+use virtualenv:
+
+ $ cd img2pdf/
+ $ virtualenv ve
+ $ ve/bin/pip3 install .
+
+You can then test the converter using:
+
+ $ ve/bin/img2pdf -o test.pdf src/tests/test.jpg
+
+If you don't want to setup Python on Windows, then head to the
+[releases](/josch/img2pdf/releases) section and download the latest
+`img2pdf.exe`.
+
+GUI
+---
+
+There exists an experimental GUI with all settings currently disabled. You can
+directly convert images to PDF but you cannot set any options via the GUI yet.
+If you are interested in adding more features to the PDF, please submit a merge
+request. The GUI is based on tkinter and works on Linux, Windows and MacOS.
+
+![](screenshot.png)
+
+Library
+-------
+
+The package can also be used as a library:
+
+ import img2pdf
+
+ # opening from filename
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg'))
+
+ # opening from file handle
+ with open("name.pdf","wb") as f1, open("test.jpg") as f2:
+ f1.write(img2pdf.convert(f2))
+
+ # opening using pathlib
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(pathlib.Path('test.jpg')))
+
+ # using in-memory image data
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert("\x89PNG...")
+
+ # multiple inputs (variant 1)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert("test1.jpg", "test2.png"))
+
+ # multiple inputs (variant 2)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(["test1.jpg", "test2.png"]))
+
+ # convert all files ending in .jpg inside a directory
+ dirname = "/path/to/images"
+ imgs = []
+ for fname in os.listdir(dirname):
+ if not fname.endswith(".jpg"):
+ continue
+ path = os.path.join(dirname, fname)
+ if os.path.isdir(path):
+ continue
+ imgs.append(path)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(imgs))
+
+ # convert all files ending in .jpg in a directory and its subdirectories
+ dirname = "/path/to/images"
+ imgs = []
+ for r, _, f in os.walk(dirname):
+ for fname in f:
+ if not fname.endswith(".jpg"):
+ continue
+ imgs.append(os.path.join(r, fname))
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(imgs))
+
+
+ # convert all files matching a glob
+ import glob
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(glob.glob("/path/to/*.jpg")))
+
+ # convert all files matching a glob using pathlib.Path
+ from pathlib import Path
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(*Path("/path").glob("**/*.jpg")))
+
+ # ignore invalid rotation values in the input images
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg'), rotation=img2pdf.Rotation.ifvalid)
+
+ # writing to file descriptor
+ with open("name.pdf","wb") as f1, open("test.jpg") as f2:
+ img2pdf.convert(f2, outputstream=f1)
+
+ # specify paper size (A4)
+ a4inpt = (img2pdf.mm_to_pt(210),img2pdf.mm_to_pt(297))
+ layout_fun = img2pdf.get_layout_fun(a4inpt)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg', layout_fun=layout_fun))
+
+ # use a fixed dpi of 300 instead of reading it from the image
+ dpix = dpiy = 300
+ layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpix, dpiy))
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg', layout_fun=layout_fun))
+
+ # create a PDF/A-1b compliant document by passing an ICC profile
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg', pdfa="/usr/share/color/icc/sRGB.icc"))
+
+Comparison to ImageMagick
+-------------------------
+
+Create a large test image:
+
+ $ convert logo: -resize 8000x original.jpg
+
+Convert it into PDF using ImageMagick and img2pdf:
+
+ $ time img2pdf original.jpg -o img2pdf.pdf
+ $ time convert original.jpg imagemagick.pdf
+
+Notice how ImageMagick took an order of magnitude longer to do the conversion
+than img2pdf. It also used twice the memory.
+
+Now extract the image data from both PDF documents and compare it to the
+original:
+
+ $ pdfimages -all img2pdf.pdf tmp
+ $ compare -metric AE original.jpg tmp-000.jpg null:
+ 0
+ $ pdfimages -all imagemagick.pdf tmp
+ $ compare -metric AE original.jpg tmp-000.jpg null:
+ 118716
+
+To get lossless output with ImageMagick we can use Zip compression but that
+unnecessarily increases the size of the output:
+
+ $ convert original.jpg -compress Zip imagemagick.pdf
+ $ pdfimages -all imagemagick.pdf tmp
+ $ compare -metric AE original.jpg tmp-000.png null:
+ 0
+ $ stat --format="%s %n" original.jpg img2pdf.pdf imagemagick.pdf
+ 1535837 original.jpg
+ 1536683 img2pdf.pdf
+ 9397809 imagemagick.pdf
+
+Comparison to pdfLaTeX
+----------------------
+
+pdfLaTeX performs a lossless conversion from included images to PDF by default.
+If the input is a JPEG, then it simply embeds the JPEG into the PDF in the same
+way as img2pdf does it. But for other image formats it uses flate compression
+of the plain pixel data and thus needlessly increases the output file size:
+
+ $ convert logo: -resize 8000x original.png
+ $ cat << END > pdflatex.tex
+ \documentclass{article}
+ \usepackage{graphicx}
+ \begin{document}
+ \includegraphics{original.png}
+ \end{document}
+ END
+ $ pdflatex pdflatex.tex
+ $ stat --format="%s %n" original.png pdflatex.pdf
+ 4500182 original.png
+ 9318120 pdflatex.pdf
+
+Comparison to podofoimg2pdf
+---------------------------
+
+Like pdfLaTeX, podofoimg2pdf is able to perform a lossless conversion from JPEG
+to PDF by plainly embedding the JPEG data into the pdf container. But just like
+pdfLaTeX it uses flate compression for all other file formats, thus sometimes
+resulting in larger files than necessary.
+
+ $ convert logo: -resize 8000x original.png
+ $ podofoimg2pdf out.pdf original.png
+ stat --format="%s %n" original.png out.pdf
+ 4500181 original.png
+ 9335629 out.pdf
+
+It also only supports JPEG, PNG and TIF as input and lacks many of the
+convenience features of img2pdf like page sizes, borders, rotation and
+metadata.
+
+Comparison to Tesseract OCR
+---------------------------
+
+Tesseract OCR comes closest to the functionality img2pdf provides. It is able
+to convert JPEG and PNG input to PDF without needlessly increasing the filesize
+and is at the same time lossless. So if your input is JPEG and PNG images, then
+you should safely be able to use Tesseract instead of img2pdf. For other input,
+Tesseract might not do a lossless conversion. For example it converts CMYK
+input to RGB and removes the alpha channel from images with transparency. For
+multipage TIFF or animated GIF, it will only convert the first frame.
+
+Comparison to econvert from ExactImage
+--------------------------------------
+
+Like pdflatex and podofoimg2pf, econvert is able to embed JPEG images into PDF
+directly without re-encoding but when given other file formats, it stores them
+just using flate compressen, which unnecessarily increases the filesize.
+Furthermore, it throws an error with CMYK TIF input. It also doesn't store CMYK
+jpeg files as CMYK but converts them to RGB, so it's not lossless. When trying
+to feed it 16bit files, it errors out with Unhandled bps/spp combination. It
+also seems to choose JPEG encoding when using it on some file types (like
+palette images) making it again not lossless for that input as well.
diff --git a/README.md b/README.md
index ef25643..8d33a36 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
+[![Travis Status](https://travis-ci.com/josch/img2pdf.svg?branch=main)](https://app.travis-ci.com/josch/img2pdf)
+[![Appveyor Status](https://ci.appveyor.com/api/projects/status/2kws3wkqvi526llj/branch/main?svg=true)](https://ci.appveyor.com/project/josch/img2pdf/branch/main)
+
img2pdf
=======
@@ -24,15 +27,15 @@ software, because the raw pixel data never has to be loaded into memory.
The following table shows how img2pdf handles different input depending on the
input file format and image color space.
-| Format | Colorspace | Result |
-| -------------------- | ------------------------------ | ------------- |
-| JPEG | any | direct |
-| JPEG2000 | any | direct |
-| PNG (non-interlaced) | any | direct |
-| TIFF (CCITT Group 4) | monochrome | direct |
-| any | any except CMYK and monochrome | PNG Paeth |
-| any | monochrome | CCITT Group 4 |
-| any | CMYK | flate |
+| Format | Colorspace | Result |
+| ------------------------------------- | ------------------------------ | ------------- |
+| JPEG | any | direct |
+| JPEG2000 | any | direct |
+| PNG (non-interlaced, no transparency) | any | direct |
+| TIFF (CCITT Group 4) | monochrome | direct |
+| any | any except CMYK and monochrome | PNG Paeth |
+| any | monochrome | CCITT Group 4 |
+| any | CMYK | flate |
For JPEG, JPEG2000, non-interlaced PNG and TIFF images with CCITT Group 4
encoded data, img2pdf directly embeds the image data into the PDF without
@@ -69,15 +72,15 @@ Bugs
when embedded into the PDF cannot be read by the Adobe Acrobat Reader,
please contact me.
- - I have not yet figured out how to determine the colorspace of JPEG2000
- files. Therefore JPEG2000 files use DeviceRGB by default. For JPEG2000
- files with other colorspaces, you must explicitly specify it using the
- `--colorspace` option.
-
- - Input images with alpha channels are not allowed. PDF doesn't support alpha
- channels in images and thus, the alpha channel of the input would have to be
- discarded. But img2pdf will always be lossless and thus, input images must
- not carry transparency information.
+ - An error is produced if the input image is broken. This commonly happens if
+ the input image has an invalid EXIF Orientation value of zero. Even though
+ only nine different values from 1 to 9 are permitted, Anroid phones and
+ Canon DSLR cameras produce JPEG images with the invalid value of zero.
+ Either fix your input images with `exiftool` or similar software before
+ passing the JPEG to `img2pdf` or run `img2pdf` with `--rotation=ifvalid`
+ (if you run img2pdf from the commandline) or by passing
+ `rotation=img2pdf.Rotation.ifvalid` as an argument to `convert()` when using
+ img2pdf as a library.
- img2pdf uses PIL (or Pillow) to obtain image meta data and to convert the
input if necessary. To prevent decompression bomb denial of service attacks,
@@ -114,6 +117,23 @@ You can then test the converter using:
$ ve/bin/img2pdf -o test.pdf src/tests/test.jpg
+If you don't want to setup Python on Windows, then head to the
+[releases](/josch/img2pdf/releases) section and download the latest
+`img2pdf.exe`.
+
+GUI
+---
+
+There exists an experimental GUI with all settings currently disabled. You can
+directly convert images to PDF but you cannot set any options via the GUI yet.
+If you are interested in adding more features to the PDF, please submit a merge
+request. The GUI is based on tkinter and works on Linux, Windows and MacOS.
+
+![](screenshot.png)
+
+Library
+-------
+
The package can also be used as a library:
import img2pdf
@@ -126,6 +146,10 @@ The package can also be used as a library:
with open("name.pdf","wb") as f1, open("test.jpg") as f2:
f1.write(img2pdf.convert(f2))
+ # opening using pathlib
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(pathlib.Path('test.jpg')))
+
# using in-memory image data
with open("name.pdf","wb") as f:
f.write(img2pdf.convert("\x89PNG...")
@@ -138,6 +162,45 @@ The package can also be used as a library:
with open("name.pdf","wb") as f:
f.write(img2pdf.convert(["test1.jpg", "test2.png"]))
+ # convert all files ending in .jpg inside a directory
+ dirname = "/path/to/images"
+ imgs = []
+ for fname in os.listdir(dirname):
+ if not fname.endswith(".jpg"):
+ continue
+ path = os.path.join(dirname, fname)
+ if os.path.isdir(path):
+ continue
+ imgs.append(path)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(imgs))
+
+ # convert all files ending in .jpg in a directory and its subdirectories
+ dirname = "/path/to/images"
+ imgs = []
+ for r, _, f in os.walk(dirname):
+ for fname in f:
+ if not fname.endswith(".jpg"):
+ continue
+ imgs.append(os.path.join(r, fname))
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(imgs))
+
+
+ # convert all files matching a glob
+ import glob
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(glob.glob("/path/to/*.jpg")))
+
+ # convert all files matching a glob using pathlib.Path
+ from pathlib import Path
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(*Path("/path").glob("**/*.jpg")))
+
+ # ignore invalid rotation values in the input images
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg'), rotation=img2pdf.Rotation.ifvalid)
+
# writing to file descriptor
with open("name.pdf","wb") as f1, open("test.jpg") as f2:
img2pdf.convert(f2, outputstream=f1)
@@ -148,6 +211,16 @@ The package can also be used as a library:
with open("name.pdf","wb") as f:
f.write(img2pdf.convert('test.jpg', layout_fun=layout_fun))
+ # use a fixed dpi of 300 instead of reading it from the image
+ dpix = dpiy = 300
+ layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpix, dpiy))
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg', layout_fun=layout_fun))
+
+ # create a PDF/A-1b compliant document by passing an ICC profile
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg', pdfa="/usr/share/color/icc/sRGB.icc"))
+
Comparison to ImageMagick
-------------------------
@@ -206,6 +279,24 @@ of the plain pixel data and thus needlessly increases the output file size:
4500182 original.png
9318120 pdflatex.pdf
+Comparison to podofoimg2pdf
+---------------------------
+
+Like pdfLaTeX, podofoimg2pdf is able to perform a lossless conversion from JPEG
+to PDF by plainly embedding the JPEG data into the pdf container. But just like
+pdfLaTeX it uses flate compression for all other file formats, thus sometimes
+resulting in larger files than necessary.
+
+ $ convert logo: -resize 8000x original.png
+ $ podofoimg2pdf out.pdf original.png
+ stat --format="%s %n" original.png out.pdf
+ 4500181 original.png
+ 9335629 out.pdf
+
+It also only supports JPEG, PNG and TIF as input and lacks many of the
+convenience features of img2pdf like page sizes, borders, rotation and
+metadata.
+
Comparison to Tesseract OCR
---------------------------
@@ -216,3 +307,15 @@ you should safely be able to use Tesseract instead of img2pdf. For other input,
Tesseract might not do a lossless conversion. For example it converts CMYK
input to RGB and removes the alpha channel from images with transparency. For
multipage TIFF or animated GIF, it will only convert the first frame.
+
+Comparison to econvert from ExactImage
+--------------------------------------
+
+Like pdflatex and podofoimg2pf, econvert is able to embed JPEG images into PDF
+directly without re-encoding but when given other file formats, it stores them
+just using flate compressen, which unnecessarily increases the filesize.
+Furthermore, it throws an error with CMYK TIF input. It also doesn't store CMYK
+jpeg files as CMYK but converts them to RGB, so it's not lossless. When trying
+to feed it 16bit files, it errors out with Unhandled bps/spp combination. It
+also seems to choose JPEG encoding when using it on some file types (like
+palette images) making it again not lossless for that input as well.
diff --git a/debian/changelog b/debian/changelog
index 1c8e09e..89dbb07 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,8 +1,125 @@
-img2pdf (0.3.3-1) UNRELEASED; urgency=medium
+img2pdf (0.5.1-1) unstable; urgency=medium
+
+ * New upstream version 0.5.1
+ - patches for imagemagick in unstable (closes: #1054762)
+ * refresh patches
+ * add debian/patches/remove-exact-cmyk8.patch
+ * add debian/patches/imagemagick-issue-285
+ * delete debian/upstream/signing-key.asc as pypi stopped shipping gpg
+ signatures
+
+ -- Johannes Schauer Marin Rodrigues <josch@debian.org> Sun, 26 Nov 2023 23:17:11 +0100
+
+img2pdf (0.4.4-4) unstable; urgency=medium
+
+ * add debian/source/options to support running 'dpkg-buildpackage -S' after
+ build (closes: #1046774)
+
+ -- Johannes Schauer Marin Rodrigues <josch@debian.org> Mon, 14 Aug 2023 14:37:03 +0200
+
+img2pdf (0.4.4-3) unstable; urgency=medium
+
+ * d/control B-D: libgs9-common -> libgs-common (Closes: #1023146)
+
+ -- Johannes Schauer Marin Rodrigues <josch@debian.org> Sun, 30 Oct 2022 19:31:17 +0100
+
+img2pdf (0.4.4-2) unstable; urgency=medium
+
+ * debian/tests/control: depend on python3-pil (>= 9.1.0-1)
+ Some tests rely on Pillow >= 9.1.0-1 because of changes related to
+ handling of GIF images that some of the tests rely on. img2pdf itself
+ will operate just fine with Pillow versions before that and just output
+ slightly different, but not broken, PDF files. For context, see #1004349,
+ #1003661 and #1004087 as well as
+ https://github.com/python-pillow/Pillow/issues/5966 and
+ https://github.com/python-pillow/Pillow/pull/6150
+
+ -- Johannes Schauer Marin Rodrigues <josch@debian.org> Wed, 20 Apr 2022 08:02:56 +0200
+
+img2pdf (0.4.4-1) unstable; urgency=medium
+
+ * New upstream version 0.4.4
+ - update for Pillow 9.1.0 (closes: #1004349)
+ * remove patches pillow9 and img2pdfprog (both applied upstream)
+ * add debian/upstream/metadata
+ * debian/patches/disable-gui.patch: add description
+ * debian/salsa-ci.yml: enable SALSA_CI_REPROTEST_ENABLE_DIFFOSCOPE
+ * debian/rules: use execute_{before,after}_dh_*
+
+ -- Johannes Schauer Marin Rodrigues <josch@debian.org> Fri, 08 Apr 2022 16:52:37 +0200
+
+img2pdf (0.4.2-2) unstable; urgency=medium
+
+ * skip test breaking with Pillow 9.0.0 (closes: #1004087, #1003661)
+
+ -- Johannes Schauer Marin Rodrigues <josch@debian.org> Tue, 25 Jan 2022 12:13:50 +0100
+
+img2pdf (0.4.2-1) unstable; urgency=medium
+
+ * New upstream version 0.4.2
+ * update patches
+ * update my name
+ * disable GUI (closes: #996149)
+ * drop dependency on python3-fitz (closes: #996150)
+ * debian/control: remove references to pdfrw
+ * debian/watch: bump to version 4
+
+ -- Johannes Schauer Marin Rodrigues <josch@debian.org> Mon, 11 Oct 2021 22:42:59 +0200
+
+img2pdf (0.4.0-1) unstable; urgency=medium
+
+ * new upstream release (closes: #966944)
+ - debian/control: add B-D on pikepdf, pytest, icc-profiles-free,
+ libgs9-common
+ - add debian/patches/remove_pdfrw.patch
+ - add debian/patches/endianess-endianness.patch
+ - add debian/patches/imdepth.patch
+ - add debian/patches/disableicc.patch
+ * debian/rules: use debian/pybuild.testfiles to do away with dh_auto_test
+ override
+ * rewrite autopkgtest to use pytest
+ * debian/control: numpy and scipy need <!nocheck>
+
+ -- Johannes 'josch' Schauer <josch@debian.org> Thu, 27 Aug 2020 09:56:16 +0200
+
+img2pdf (0.3.6-1) unstable; urgency=medium
* new upstream release
+ * debian/control: drop b-d on python3-pdfrw (closes: #958362)
+ * debian/control: bump dh compat to 13
+
+ -- Johannes 'josch' Schauer <josch@debian.org> Sat, 09 May 2020 08:32:20 +0200
+
+img2pdf (0.3.4-1) unstable; urgency=medium
- -- Johannes 'josch' Schauer <josch@debian.org> Mon, 07 Jan 2019 10:57:46 +0100
+ * new upstream release
+ - don't use flaky imagemagick to generate testinput (closes: #955564)
+ * let manpage refer to img2pdf, not img2pdf.py (closes: #922180)
+ * add debian/salsa-ci.yml
+ * debian/control: move vcs to salsa
+ * debian/tests/default: ADTTMP -> AUTOPKGTEST_TMP
+ * debian/control: add Rules-Requires-Root: no
+ * bump dh compat to 12
+ * debian/control: add build-depends on python3-numpy and python3-scipy
+ * debian/tests/control: format with wrap-and-sort
+ * debian/control: bump standards-version to 4.5.0 (no changes)
+ * debian/tests/control: add depends of test-sh on python3-numpy and
+ python3-scipy
+ * also install the GUI
+
+ -- Johannes 'josch' Schauer <josch@debian.org> Sun, 05 Apr 2020 21:16:58 +0200
+
+img2pdf (0.3.3-1) unstable; urgency=medium
+
+ * new upstream release
+ * debian/control: run wrap-and-sort
+ * debian/upstream/signing-key.asc: export a more minimal key with
+ --export-options export-minimal,export-clean
+ * debian/rules: run test.sh during build
+ * add test.sh autopkgtest
+ * debian/control: bump Standards-Version to 4.3.0
+
+ -- Johannes 'josch' Schauer <josch@debian.org> Mon, 07 Jan 2019 15:24:31 +0100
img2pdf (0.3.2-1) unstable; urgency=medium
diff --git a/debian/compat b/debian/compat
deleted file mode 100644
index b4de394..0000000
--- a/debian/compat
+++ /dev/null
@@ -1 +0,0 @@
-11
diff --git a/debian/control b/debian/control
index 9a5b6e1..9eb1f6f 100644
--- a/debian/control
+++ b/debian/control
@@ -1,25 +1,31 @@
Source: img2pdf
-Maintainer: Johannes 'josch' Schauer <josch@debian.org>
+Maintainer: Johannes Schauer Marin Rodrigues <josch@debian.org>
Section: python
Priority: optional
-Build-Depends: debhelper (>= 11),
+Build-Depends: debhelper-compat (= 13),
dh-python,
ghostscript <!nocheck>,
help2man,
+ icc-profiles-free <!nocheck>,
imagemagick <!nocheck>,
+ libgs-common <!nocheck>,
libimage-exiftool-perl <!nocheck>,
libtiff-tools <!nocheck>,
mupdf-tools <!nocheck>,
netpbm <!nocheck>,
poppler-utils <!nocheck>,
python3-all,
- python3-pdfrw,
+ python3-numpy <!nocheck>,
+ python3-pikepdf <!nocheck>,
python3-pil,
+ python3-pytest <!nocheck>,
+ python3-scipy <!nocheck>,
python3-setuptools
-Standards-Version: 4.3.0
-Vcs-Browser: https://browse.dgit.debian.org/img2pdf.git/
-Vcs-Git: https://git.dgit.debian.org/img2pdf
+Standards-Version: 4.5.0
+Vcs-Browser: https://salsa.debian.org/debian/img2pdf
+Vcs-Git: https://salsa.debian.org/debian/img2pdf.git
Homepage: https://gitlab.mister-muffin.de/josch/img2pdf
+Rules-Requires-Root: no
Package: img2pdf
Architecture: all
@@ -44,7 +50,6 @@ Description: Lossless conversion of raster images to PDF
Package: python3-img2pdf
Architecture: all
Depends: ${misc:Depends}, ${python3:Depends}
-Suggests: python3-pdfrw
Breaks: ocrmypdf (<< 6.2.3-1)
Description: Lossless conversion of raster images to PDF (library)
This module will take a list of raster images and produce a PDF file with the
@@ -57,7 +62,4 @@ Description: Lossless conversion of raster images to PDF (library)
case of JPEG and JPEG2000 images) or equal (in case of other formats) than
that of existing tools.
.
- Img2pdf includes its own PDF writer but will use the pdfrw module if
- available instead.
- .
This package contains the Python library.
diff --git a/debian/copyright b/debian/copyright
index 24b04a5..4aa1601 100644
--- a/debian/copyright
+++ b/debian/copyright
@@ -3,7 +3,7 @@ Upstream-Name: img2pdf
Source: https://gitlab.mister-muffin.de/josch/img2pdf
Files: *
-Copyright: 2014 Johannes Schauer <josch@debian.org>
+Copyright: 2014 - 2021 Johannes Schauer Marin Rodrigues <josch@debian.org>
License: LGPL-3
This program is free software; you can redistribute it
and/or modify it under the terms of the Lesser GNU General Public
@@ -15,7 +15,7 @@ License: LGPL-3
Files: src/jp2.py
Copyright:
- 2014 Johannes Schauer <josch@debian.org>
+ 2014 Johannes Schauer Marin Rodrigues <josch@debian.org>
KB / National Library of the Netherlands, Open Planets Foundation
License: LGPL-3+
This program is free software: you can redistribute it and/or modify
diff --git a/debian/img2pdf.install b/debian/img2pdf.install
index 2f5f524..68a2395 100644
--- a/debian/img2pdf.install
+++ b/debian/img2pdf.install
@@ -1 +1,3 @@
usr/bin/img2pdf
+# deactivate gui until it becomes usable
+#usr/bin/img2pdf-gui
diff --git a/debian/not-installed b/debian/not-installed
new file mode 100644
index 0000000..ebd7f3e
--- /dev/null
+++ b/debian/not-installed
@@ -0,0 +1 @@
+usr/bin/img2pdf-gui
diff --git a/debian/patches/disable-gui.patch b/debian/patches/disable-gui.patch
new file mode 100644
index 0000000..341d64a
--- /dev/null
+++ b/debian/patches/disable-gui.patch
@@ -0,0 +1,27 @@
+From: Johannes Schauer Marin Rodrigues <josch@debian.org>
+Subject: disable gui as it's barely functional
+Forwarded: not-needed
+
+--- a/src/img2pdf.py
++++ b/src/img2pdf.py
+@@ -3989,9 +3989,6 @@ Report bugs at https://gitlab.mister-muf
+ help="Prints version information and exits.",
+ )
+ parser.add_argument(
+- "--gui", dest="gui", action="store_true", help="run experimental tkinter gui"
+- )
+- parser.add_argument(
+ "--from-file",
+ metavar="FILE",
+ type=from_file,
+@@ -4406,10 +4403,6 @@ def main(argv=sys.argv):
+ if args.pillow_limit_break:
+ Image.MAX_IMAGE_PIXELS = None
+
+- if args.gui:
+- gui()
+- sys.exit(0)
+-
+ layout_fun = get_layout_fun(
+ args.pagesize, args.imgsize, args.border, args.fit, args.auto_orient
+ )
diff --git a/debian/patches/imagemagick-issue-285 b/debian/patches/imagemagick-issue-285
new file mode 100644
index 0000000..28a480f
--- /dev/null
+++ b/debian/patches/imagemagick-issue-285
@@ -0,0 +1,25 @@
+Subject: https://github.com/ImageMagick/ImageMagick6/issues/285
+From: Johannes Schauer Marin Rodrigues <josch@debian.org>
+
+--- a/src/img2pdf_test.py
++++ b/src/img2pdf_test.py
+@@ -5582,6 +5582,9 @@ def test_jpg_2000(tmp_path_factory, jpg_
+ @pytest.mark.skipif(
+ not HAVE_JP2, reason="requires imagemagick with support for jpeg2000"
+ )
++@pytest.mark.skipif(
++ True, reason="https://github.com/ImageMagick/ImageMagick6/issues/285"
++)
+ def test_jpg_2000_rgba8(tmp_path_factory, jpg_2000_rgba8_img, jpg_2000_rgba8_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg_2000_rgba8")
+ compare_ghostscript(tmpdir, jpg_2000_rgba8_img, jpg_2000_rgba8_pdf)
+@@ -5597,6 +5600,9 @@ def test_jpg_2000_rgba8(tmp_path_factory
+ @pytest.mark.skipif(
+ not HAVE_JP2, reason="requires imagemagick with support for jpeg2000"
+ )
++@pytest.mark.skipif(
++ True, reason="https://github.com/ImageMagick/ImageMagick6/issues/285"
++)
+ def test_jpg_2000_rgba16(tmp_path_factory, jpg_2000_rgba16_img, jpg_2000_rgba16_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg_2000_rgba16")
+ compare_ghostscript(
diff --git a/debian/patches/remove-exact-cmyk8.patch b/debian/patches/remove-exact-cmyk8.patch
new file mode 100644
index 0000000..a0f00de
--- /dev/null
+++ b/debian/patches/remove-exact-cmyk8.patch
@@ -0,0 +1,85 @@
+Subject: remove HAVE_EXACT_CMYK8
+From: Johannes Schauer Marin Rodrigues <josch@debian.org>
+
+--- a/src/img2pdf_test.py
++++ b/src/img2pdf_test.py
+@@ -85,7 +85,6 @@ for prog in ["convert", "compare", "iden
+ globals()[prog.upper()] = ["magick", prog]
+
+ HAVE_IMAGEMAGICK_MODERN = True
+-HAVE_EXACT_CMYK8 = True
+ try:
+ ver = subprocess.check_output(CONVERT + ["-version"], stderr=subprocess.STDOUT)
+ m = re.fullmatch(
+@@ -93,18 +92,13 @@ try:
+ )
+ if m is None:
+ HAVE_IMAGEMAGICK_MODERN = False
+- HAVE_EXACT_CMYK8 = False
+ else:
+ if parse_version(m.group(1)) < parse_version("6.9.10-12"):
+ HAVE_IMAGEMAGICK_MODERN = False
+- if parse_version(m.group(1)) < parse_version("7.1.0-48"):
+- HAVE_EXACT_CMYK8 = False
+ except FileNotFoundError:
+ HAVE_IMAGEMAGICK_MODERN = False
+- HAVE_EXACT_CMYK8 = False
+ except subprocess.CalledProcessError:
+ HAVE_IMAGEMAGICK_MODERN = False
+- HAVE_EXACT_CMYK8 = False
+
+ if not HAVE_IMAGEMAGICK_MODERN:
+ warnings.warn("imagemagick >= 6.9.10-12 not available, skipping certain checks...")
+@@ -351,9 +345,7 @@ def write_png(data, path, bitdepth, colo
+
+ def compare(im1, im2, exact, icc, cmyk):
+ if exact:
+- if cmyk and not HAVE_EXACT_CMYK8:
+- raise Exception("cmyk cannot be exact before ImageMagick 7.1.0-48")
+- elif icc:
++ if icc:
+ raise Exception("icc cannot be exact")
+ else:
+ subprocess.check_call(
+@@ -5562,11 +5554,9 @@ def test_jpg_rot(tmp_path_factory, jpg_r
+ )
+ def test_jpg_cmyk(tmp_path_factory, jpg_cmyk_img, jpg_cmyk_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg_cmyk")
+- compare_ghostscript(
+- tmpdir, jpg_cmyk_img, jpg_cmyk_pdf, gsdevice="tiff32nc", exact=HAVE_EXACT_CMYK8
+- )
++ compare_ghostscript(tmpdir, jpg_cmyk_img, jpg_cmyk_pdf, gsdevice="tiff32nc")
+ # not testing with poppler as it cannot write CMYK images
+- compare_mupdf(tmpdir, jpg_cmyk_img, jpg_cmyk_pdf, exact=HAVE_EXACT_CMYK8, cmyk=True)
++ compare_mupdf(tmpdir, jpg_cmyk_img, jpg_cmyk_pdf, cmyk=True)
+ compare_pdfimages_cmyk(tmpdir, jpg_cmyk_img, jpg_cmyk_pdf)
+
+
+@@ -5957,12 +5947,9 @@ def test_tiff_cmyk8(tmp_path_factory, ti
+ tiff_cmyk8_img,
+ tiff_cmyk8_pdf,
+ gsdevice="tiff32nc",
+- exact=HAVE_EXACT_CMYK8,
+ )
+ # not testing with poppler as it cannot write CMYK images
+- compare_mupdf(
+- tmpdir, tiff_cmyk8_img, tiff_cmyk8_pdf, exact=HAVE_EXACT_CMYK8, cmyk=True
+- )
++ compare_mupdf(tmpdir, tiff_cmyk8_img, tiff_cmyk8_pdf, cmyk=True)
+ compare_pdfimages_tiff(tmpdir, tiff_cmyk8_img, tiff_cmyk8_pdf)
+
+
+@@ -6403,11 +6390,9 @@ def test_tiff_ccitt_nometa2(
+ )
+ def test_miff_cmyk8(tmp_path_factory, miff_cmyk8_img, tiff_cmyk8_img, miff_cmyk8_pdf):
+ tmpdir = tmp_path_factory.mktemp("miff_cmyk8")
+- compare_ghostscript(
+- tmpdir, tiff_cmyk8_img, miff_cmyk8_pdf, gsdevice="tiff32nc", exact=False
+- )
++ compare_ghostscript(tmpdir, tiff_cmyk8_img, miff_cmyk8_pdf, gsdevice="tiff32nc")
+ # not testing with poppler as it cannot write CMYK images
+- compare_mupdf(tmpdir, tiff_cmyk8_img, miff_cmyk8_pdf, exact=False, cmyk=True)
++ compare_mupdf(tmpdir, tiff_cmyk8_img, miff_cmyk8_pdf, cmyk=True)
+ compare_pdfimages_tiff(tmpdir, tiff_cmyk8_img, miff_cmyk8_pdf)
+
+
diff --git a/debian/patches/series b/debian/patches/series
new file mode 100644
index 0000000..80f3c3b
--- /dev/null
+++ b/debian/patches/series
@@ -0,0 +1,3 @@
+disable-gui.patch
+remove-exact-cmyk8.patch
+imagemagick-issue-285
diff --git a/debian/pybuild.testfiles b/debian/pybuild.testfiles
new file mode 100644
index 0000000..3459eb6
--- /dev/null
+++ b/debian/pybuild.testfiles
@@ -0,0 +1,2 @@
+src/img2pdf_test.py
+src/tests
diff --git a/debian/rules b/debian/rules
index b525322..57e9340 100755
--- a/debian/rules
+++ b/debian/rules
@@ -1,18 +1,15 @@
#!/usr/bin/make -f
+# set img2pdf location for pytest run by dh_auto_test
+# we cannot use the version copied by pybuild into
+# .pybuild/cpython3_3.8/build because that has the executable bit removed
+export img2pdfprog = $(CURDIR)/src/img2pdf.py
+
%:
dh $@ --with python3 --buildsystem=pybuild
-override_dh_auto_clean:
+execute_before_dh_auto_clean:
rm -f img2pdf.1
- dh_auto_clean
-
-override_dh_auto_build:
- dh_auto_build
- help2man --no-info --name="lossless conversion of raster images to pdf" ./src/img2pdf.py -o img2pdf.1
-override_dh_auto_test:
- dh_auto_test
-ifeq (,$(filter nocheck,$(DEB_BUILD_OPTIONS) $(DEB_BUILD_PROFILES)))
- ./test.sh
-endif
+execute_after_dh_auto_build:
+ help2man --no-info --name="lossless conversion of raster images to pdf" ./src/img2pdf.py | sed -e 's/\.py//ig;' > img2pdf.1
diff --git a/debian/salsa-ci.yml b/debian/salsa-ci.yml
new file mode 100644
index 0000000..d598aba
--- /dev/null
+++ b/debian/salsa-ci.yml
@@ -0,0 +1,8 @@
+include:
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/salsa-ci.yml
+ - https://salsa.debian.org/salsa-ci-team/pipeline/raw/master/pipeline-jobs.yml
+
+variables:
+ SALSA_CI_DISABLE_BLHC: 1
+ SALSA_CI_DISABLE_BUILD_PACKAGE_ANY: 1
+ SALSA_CI_REPROTEST_ENABLE_DIFFOSCOPE: 1
diff --git a/debian/source/options b/debian/source/options
new file mode 100644
index 0000000..d98bc97
--- /dev/null
+++ b/debian/source/options
@@ -0,0 +1 @@
+extend-diff-ignore = "^src/img2pdf\.egg-info/(PKG-INFO|SOURCES\.txt|entry_points\.txt)$"
diff --git a/debian/tests/control b/debian/tests/control
index ac75dd0..5d96b8a 100644
--- a/debian/tests/control
+++ b/debian/tests/control
@@ -1,7 +1,17 @@
-Tests: default
+Tests: pytest
Restrictions: allow-stderr
-Depends: @, python3-pdfrw
-
-Tests: test-sh
-Restrictions: allow-stderr
-Depends: @, ghostscript, imagemagick, libimage-exiftool-perl, libtiff-tools, mupdf-tools, netpbm, poppler-utils
+Depends: ghostscript,
+ icc-profiles-free,
+ imagemagick,
+ libgs9-common,
+ libimage-exiftool-perl,
+ libtiff-tools,
+ mupdf-tools,
+ netpbm,
+ poppler-utils,
+ python3-numpy,
+ python3-pikepdf,
+ python3-pytest,
+ python3-scipy,
+ python3-pil (>= 9.1.0-1),
+ @
diff --git a/debian/tests/default b/debian/tests/default
deleted file mode 100644
index 9a0f42d..0000000
--- a/debian/tests/default
+++ /dev/null
@@ -1,12 +0,0 @@
-#!/bin/sh
-set -exu
-
-for f in src/tests/input/*; do
- bn=$(basename "$f")
- pdfrw=
- if [ "${f%.gif}" != "$f" ]; then
- pdfrw=--without-pdfrw
- fi
- img2pdf $pdfrw --nodate --producer="" "$f" -o "$ADTTMP/$bn.pdf"
- diff -u --text "src/tests/output/$bn.pdf" "$ADTTMP/$bn.pdf"
-done
diff --git a/debian/tests/pytest b/debian/tests/pytest
new file mode 100755
index 0000000..1eb94b2
--- /dev/null
+++ b/debian/tests/pytest
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+img2pdfprog=/usr/bin/img2pdf python3 -m pytest
diff --git a/debian/tests/test-sh b/debian/tests/test-sh
deleted file mode 100755
index fbdf1be..0000000
--- a/debian/tests/test-sh
+++ /dev/null
@@ -1,3 +0,0 @@
-#!/bin/sh
-
-img2pdfprog=/usr/bin/img2pdf sh -x ./test.sh
diff --git a/debian/upstream/metadata b/debian/upstream/metadata
new file mode 100644
index 0000000..297c90f
--- /dev/null
+++ b/debian/upstream/metadata
@@ -0,0 +1,7 @@
+Archive: pypi.python.org
+Bug-Database: https://gitlab.mister-muffin.de/josch/img2pdf/issues
+Bug-Submit: https://gitlab.mister-muffin.de/josch/img2pdf/issues/new
+Changelog: https://gitlab.mister-muffin.de/josch/img2pdf/src/branch/main/CHANGES.rst
+Documentation: https://manpages.debian.org/bullseye/img2pdf/img2pdf.1.en.html
+Repository: https://gitlab.mister-muffin.de/josch/img2pdf.git
+Repository-Browse: https://gitlab.mister-muffin.de/josch/img2pdf
diff --git a/debian/upstream/signing-key.asc b/debian/upstream/signing-key.asc
deleted file mode 100644
index 3bd21f3..0000000
--- a/debian/upstream/signing-key.asc
+++ /dev/null
@@ -1,110 +0,0 @@
------BEGIN PGP PUBLIC KEY BLOCK-----
-
-mQINBFHVWP0BEAC4vnKRkDgoQ4JrRHhDrKipbs4I0xwRSDHlhnD1bsa12PNaJytH
-HUufulM5woChwGPFOH0Ex0eOzFWzQ1cHmijIIdm5h9tGSxQK+AF5lh2q9/ae1SXW
-bh9u+6u8PWS1P9nxXMCN9c4ahwUb5YYCH2ThkmlhzvAeX0/hk85zecglsypUfQgO
-9tp72S8CX/Lx0HX0at7xEioKgA39/ZWD4b7FktI3MX+UYMgOXsgsWqmY2gMGUp3E
-3Aa6se6/63nhY3HLCCHUYS3pxP7Cnw5fI3/KJ9yBSGQ8LoNwijJtJD0XWTaUikKy
-+MrifZDpfFIxvo/JJJLOXTi7nEnXZitKV5pz49/6CkhbSdAt423honj+Gn58viUw
-pyMjpfCaZu4/RN8GLDMvlz2etst0HHnINIwQWPrXLubF3jqe8uhseKATO7FkgaOw
-4o6xC+NZuycT7pXsb6m51y/TZfAq/eTP0TE8jMSVf1dpMoyLOcI4VciL26G7uujQ
-qjdBVIgcPnv7XG9y+HqHX49pvTRo0Sum21LpbZRDCeRtYe8flbBvHzM+B+S93xSn
-uppct4BFrKJ2RU7QCIpDOBvfP24cy9Nu+AphScXw5FtQOzKfz3+kosPz3uu0XCUt
-xX7h8s00dT4hQQX/bMwjqa2WJNnoaqg5oIPMRNkZHPuPNcsdobSy3KJuWwARAQAB
-tClKb2hhbm5lcyBTY2hhdWVyIDxqb3NjaEBtaXN0ZXItbXVmZmluLmRlPokCOgQT
-AQgAJAIbAwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAUCUdVbIQIZAQAKCRB9XYxg
-z00+tHLgD/4xU2nGfP187CQm4d4cZG7maaLSWeyffY2UMgBYwppioIv99hPWFV8j
-ePsBsAYGXH0TWaci+mLAiYveirGFnl4atwc9+YvWE5J1zot6nE6cFdv7YM6X4WaO
-/5zjkI0uXXOckfDrT8HbTDhQvyrkLL3NB728lHkPZrTFxrmCJkWZb9zU2X4Mbp/M
-Rhei1OWZu3FiOxc7or5dbfWL+t8XEmQ/CrO96f2rHOjcuk+o//4xcm1uDFbKS385
-OjtE6rcDowWvDGvd1IYIyFdXinCX0vZcTSrsgUknMHpOb//ucKiSC9tjNQlqXyAx
-yok7KAyJrXRflY0ja2EEVywfWaw99H5o5tQq5utNnG/XitS98qEk4w8P6OC4FInr
-uNgxyTcBFBAE/M2HnRCRW+1Tui1KB0gKAY09NhxX++6EbaetXqjiHBLnNBcOaSWZ
-FMjzo/qn7YqRljAlLwHACK03J8yvbcIfFW4uTBpmE2+Y8vR+JaegW2+Xh94OVPeH
-Q8y8GRkCwkZxBxNIZGjjm9MWuccGHxQtzU6balfYyAgoC1HvnSw8parqRW6kkl7h
-TBJVcbJkJ37XRNVtRGPmdg98xrAk2MUZOFMOKBXFQW4e3a3KNRO8zQh7x/tmfG/R
-ti3yIjpHLhGrkwaMOUdAz/Fh43ZNcR8mI2PIF7Aj4eA+/B/dyua9DbQlSm9oYW5u
-ZXMgU2NoYXVlciA8ai5zY2hhdWVyQGVtYWlsLmRlPokCNwQTAQgAIQUCUdVa4QIb
-AwULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRB9XYxgz00+tDVlD/9BqE0Kde9M
-oLIBkE2pOtS59yZRQ+omL2twIbLx58cyH1WY+iTtgzctJU2nbbBs2aW/pMVTuSUg
-qEnwM+BO/OM+ltFdwuVbZeO3eTLjkDKbArqnx49os4/TsvgLLzo+VbMRZcuo1C2g
-yJ52/KlQ8pWIr1UoIpNKvIdFTCC96mYZMxiLffYfKnCCOz5SOoYy9atzOZ8CAZOo
-iZSk+ofoR+XP85qWr5/myi6jUV8vzKJdPDDjsFrZhKZNVkDNhfox5epk+nxej+ga
-nF8GPTKA/0IYxbo3Ofbf7ecIBwVtI7U9SzT2TExex++ZlTF3f4WjhzMNX17zD02x
-HFc02GGSbdNzhZAYOZn4hM0qgMkZL7VR1ZT4x89qH58pnzv87VHT5Jfj1r++pMLO
-sewbZAbmMj97EVHTXgZ2VSBza7s1nTd2Te1v/NNnkHIPg096/LdKIRSwZMbR/nN/
-ax5KCqZIVFCFvlPjEXPfCs5Djw93p8c07twG4O1HxnF6cms0LoUIuy86+4o5eq97
-RufcLwSLmu2k5ab4ez89+ZI87yD+70KnixzYA3WKUFmi/ClvGaMS3/N6+th03TkC
-xiiYA9ZJyyKssO2WgiII3vFZup1dZkDFWjje5aBhtGyFJikWsPJjT203ZuxvqJ5Z
-2S+driyy3Vc6kxdiTSs0sqTULKAdmlzSybQjSm9oYW5uZXMgU2NoYXVlciA8am9z
-Y2hAZGViaWFuLm9yZz6JAjcEEwEIACEFAlTSH1oCGwMFCwkIBwMFFQoJCAsFFgID
-AQACHgECF4AACgkQfV2MYM9NPrRBJxAAheF1zMIsWRSMtmZaTe0gr7f5pjcxaD2a
-CaeVQg5PAlx+cLF0/AYVbFDVzAXQq+jefgnddqw897ZAL8SsCUIcTLurvO3q4qx1
-hg+kqhVzFyji3S49i3jlB4ozmA1VMZKGhqzLDN/UYIXwfvkLmp4TVw0KMy7Fmmrl
-aIYcRexBsyf8u3r6OuorPJHRxxN2cIMWLEtXhOYSk8tOzMpKBgj5Ki4g6ORW5+3T
-SBTBpwlZAuRb5cGmiiRdgc/R/gTGlMYJr2OB/s/38n1K8qvcS2JBXfN29MAJDeDR
-kNKiI3Xe4S3f7bdcuPBuh45OFWM/vOt1jhsqzVR0EFb74ok4M/f3mnKeZzMlVnGd
-XoePzx6Z4Ocva029jaD8utnl66Pd/6O8lqwpriDurnFQ495zchWssn0RCcv0CmkK
-8qj9+ru6TOyAarBKbu9C1hDlIGkBaYlM/jVm+e+anoki8nbulWG4YHH5KykQWyW4
-KjqI1k3XItLZJzYUJv2xLmYlAo+5f/aWh5aNuorGOGaKg8FIFyUysnePxSHmeL33
-kP6GO16EZDUNsrJMkFo0qfMyJm3wrDTqe4BzYMj3lAT2FQ1PvpuLuQdnN+iplUWs
-XOr14BJfBMbr5Gdy9ADOL5RvkY/5zEZDAI2OGgOCyB6ut8j+F7IPjaKOWaRxjsBq
-UvodwoeUt6W5Ag0EUdVY/QEQANiGeE/TTLZt7x/14zmvM+6vRh4VpX4eovu74RMp
-n62hVE17vTuCRTh+1IiFWb3URT9ciQsqWdJxHE4omJWTHSZJP/QnCYgqBjBZqQZ3
-TLo+B46f5ZDgF7Sojc1HcNIL2gE3ZWubQfFNjnJF3nhLh6L+mql9k4I8Nfy9Q0P/
-+9ob/Ze+AQuG+ZKS4AKb/QO6D+jePonSS49F6cbCZWQy0gQAAGHKmCgWzJ7Xy464
-6+KCMO2a8+4N5TSh3g7nemMS58Mx7mX7g9ong/v7r5/ee/04xcxdcrzbvIqf+6+s
-y/NOkwCf53SpuEorlKNfkoiow99FBjqlGFI/x9J7lTKdxwBmUkQPVVeFJtm/edr+
-kH/sfG3Vc+eNgniLdiQC/BUeTA5jHKOMp6rJmfwsOS3N82EktL28ztCdxYWN+EQM
-twQ39E0f/jYYqWpRflobvjhkTLO1flyNH9rbW3OEdF/tGBnJVm3+bqz2ZjGfUBwM
-X4neID/2mh7BBIBe4wAljUq3AXLeKlIHwBcoNHN8bIBI371rwgMK0sjeAxrQ8awN
-YRz4G3WaU5OLxZOBqbD3t3S2fbgI3LXmBtn+c6OlVRyKDkVyrh7uvtlN5kW9zdTA
-ri1gvADd2mDPSU+DzlTyPXiMb5IlwG/nglVo3beCygfUFi62gPR+eAMPsq4r/yly
-+y3XABEBAAGJAh8EGAEIAAkFAlHVWP0CGwwACgkQfV2MYM9NPrSrRRAAsOFu2Cfz
-mDbbMrgtXzBSS2hgD/MD+E62dDcEw6nO5HvSwBET1SmaEF7D9Lt9rQYsCEZew4A1
-b8bnUzwDfM/3DMZdw/j8HH6pJtjHjfQLNMql4adgQ/kCESXtJAB00lr6AaFAaiQH
-A09l4TxprugoHfi+iriZWaq35YCIIDRnnXeaMJxRb9I0eCT7d4LKsDBd1xrxDc1T
-Uf+AQluYoowmVaicuCLXkKenLMPmU+goXfDPPldjpVyDKiWjDCqwXnZdinILSWkR
-go3s0BK8sP8cG4LY6hlG+56u6RolCp1AOXf6qsO25XsYgsTdazOtd1o2YPxMtbeL
-ZC+0xwTpfxz1uN2TP0FkIM1gPOOp5MnVZf1S0R39uRZn0rzby3ExoBaHgFDiqUOG
-YPuiL/HFKcatLt728TQVw/ylZdgOSBm293vhAlHS+gUch6DuAGErQDHUt0IaWZuR
-L/Z9S82hu3yomD14JL6sM7qdFLygooQKFI5xFpRDn+2IoQN3dhdQKI2WtfcqVg5M
-FKMPD6oIDNjqkuMIHxWqRrfKQIWIglVGKhqnlUGIBWksEBOOJVad+DhZ+FXdVNQl
-R9IAbKq99Qu3ylF6FpbJ72RRh4PCG5N/6TrMkhiHiAa7qa3s/ka9EpaEWfXRy3kn
-dQ0zzLIA1ubSYO5pTm/HRqV0NxT1AFZSoOe5Ag0EUdVcRQEQALzAiJCMWIVb+rx9
-b8TeHvWkSdGX/bsNZuEwDrKaEW8DgBi5BpPGpSPaxzGphdorO0kiyWL+YE45q3x0
-n1gTPT4M3pUhEBL6l+CPFaMYQ3Zc0OKJJ41KY2k5Ot3hxtfhtYvtwiVl6mi+Mvd8
-Wq0zygYmI2w3JDSjmUHmfmloEK0zByrFaUf3+IPtP0T5Jpq5/+/+cVr5KYPuS3oV
-3NAfSJkkhF3KOouP6RkdwL1FrhQihEI4gjN7YO7aNh84+ERH5JvSl1KTwt6VaV4N
-jcjua1GUIthehSFXItp7R6XjcTvnOwvrtsBKbuyRfhRiy839GZnopKcotg1gjTc7
-scaCIkXQwCWQxpZc1oH9mJPLTdx25GtK23IkSroO6RPcnOVqWGDenwsYlN9+XCZf
-XBa4fPjlhOxHN5s+LNA6d7PspAVlzy10SicUbHsjM52K6Wob3VsfoN/tKP2Rvhv5
-ISDV0FNlMXrL5t7fehj06iyaLANnEu/svWkQREZiWiIzd44noY1ZoDYKL8PYYYbZ
-2eH1txCBypZWjse4s2waBzUpLGhZ5k/ahTodTqqcGK2p/5mhipeYj9BGk1GnZfYD
-XqIEKKDF8cnlU7wFXwaovInuhbTysLKrKlyDX0kChCZjSc249FY+uM1GnoEqys61
-ZsVhHYhKl+/wSp42r/RvIqYDB7pHABEBAAGJBD4EGAEIAAkFAlHVXEUCGwICKQkQ
-fV2MYM9NPrTBXSAEGQEIAAYFAlHVXEUACgkQ8sulx4+9g+GpTw//VoJA2lCQ7jXa
-juM0ze3zkWdye5TL5871hnFl/a3i3v9WWj+8fEva2yAjY0WVXH9/pWdoqX7rtPXg
-BJ6C96MijzWDDao9fZZkLLZVuyNyNakB02sceuXIC50mo/OnmNDUvTh9WL1w6z8E
-LMdNw/Vd3030GxQ19ySc3SruDLx8qZeDzp7pE69cYmXmJa+rkH92tchQRQrxM6EN
-qN9O2ZAIEx2XAIg61D5DOvmT5SY3rRJpGApeGuPo1rpQlgxWZL+jQk0BT5Ix7Ojn
-vv2c94XYfB7sgZ24Sp0VP553fPjMu4cUXKTR5fiRL1bNWBAPTGd1t6XkUzfoUnot
-ahvqi3DJJXpn4adJMzJ54nZtjp/++Gb+GCCxhfDFZQvLXPLJfeNGciuGESVN4xl9
-o1G6sb9ohNSnDhAoR8TRDclON8I895OsQblDwGAdqMna7SwzE6hB/9K7+z82IEKJ
-nJC6zfjyD2WuMQUdORPn7MuH1ZuHakPllH+zC8llhozA87Cw95lyLA8v6LzQqXtM
-cLpG1Aqp3ncVbFplmXwGJMSBZnzOuZcgeqVUJ0J9BKVUKDOhjfnW+sc5RMN5zAW7
-L7XtTMU63QFFciSXiK41PD39waVfo9xV//urgAZbSXG6KNAcy1NjMlfV0S8Gh1qk
-jIAa3ig8JTIB53WhI2FPZXYXsx62fXHl3Q//ZI4biIKsi9BP1qFEz9hV0sSnUIt8
-r+BRoBcBhG/OzCBjestnm2zw1MFKkUcCTm9SpfvA77cEJrUPdlWH72+XkxPvhzwn
-LkWBdTm1ZOZbJvWulUOY47+JsVl7YNhTvk9eIssJ4qd0EWEmdFVDAWtyEIeymsz5
-bfiSPfq8dmSk9uTS/kuhU9bHhh404dlrpEPcgzFOVwx6mW33HcAsRtasTdqKH38i
-ZbBeaPp1mYTXGrPyG/KKs0lsAdTg0fjUqmo4K7Qa1c+a3M7gjMGejKPmBnQMNL4h
-PSK1+ejuOQRaNnbd3v9VYuFbtG/A9E36Jb00DxAGLhW/hmcsvYdZ4D48XLKymYOa
-zeRPtYJ4TsBGm+3BZzgXHnHQXRrG3iJZEeM1E+XHTnP04SDTaDCIAr03YMmK/nWO
-Q14K7QihDdmxIb3g5Qs4TXmh7B33sBtfGFla0pt0lNYPx3v85IlWOBQsDN6QLpM/
-KAEueXdSpK//Je+d3pbx2f79vD/k/1/ZeFyQ/FSFj+nJcAwmfZHOrzxhTV4ioZ8R
-va8FK3KkJGhJwW58N0IXaE3kx+f2gp4XaIBzdMl+XpUMGFaMN+knjcfiCdT+NtiO
-4ZKfMf0dEA/PJJcBgL/BaZWQznP0zZUXxkd/KDUBNZ3gjfePgZMno/vjY0YsdZC1
-G3jSKFcffBYpTE4=
-=UNfd
------END PGP PUBLIC KEY BLOCK-----
diff --git a/debian/watch b/debian/watch
index 755e317..9ee5474 100644
--- a/debian/watch
+++ b/debian/watch
@@ -1,4 +1,4 @@
# please also check http://pypi.debian.net/img2pdf/watch
-version=3
-opts=uversionmangle=s/(rc|a|b|c)/~$1/,pgpsigurlmangle=s/$/.asc/ \
+version=4
+opts=uversionmangle=s/(rc|a|b|c)/~$1/ \
https://pypi.debian.net/img2pdf/img2pdf-(.+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
diff --git a/setup.cfg b/setup.cfg
index 9f88734..8bfd5a1 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,3 @@
-[metadata]
-description-file = README.md
-
[egg_info]
tag_build =
tag_date = 0
diff --git a/setup.py b/setup.py
index b0438fe..2b7c3e0 100644
--- a/setup.py
+++ b/setup.py
@@ -1,62 +1,51 @@
import sys
from setuptools import setup
-PY3 = sys.version_info[0] >= 3
-
-VERSION = "0.3.3"
+VERSION = "0.5.1"
INSTALL_REQUIRES = (
- 'Pillow',
-)
-
-TESTS_REQUIRE = (
- 'pdfrw',
+ "Pillow",
+ "pikepdf",
)
-if not PY3:
- INSTALL_REQUIRES += ('enum34',)
-
-
setup(
- name='img2pdf',
+ name="img2pdf",
version=VERSION,
- author="Johannes 'josch' Schauer",
- author_email='josch@mister-muffin.de',
+ author="Johannes Schauer Marin Rodrigues",
+ author_email="josch@mister-muffin.de",
description="Convert images to PDF via direct JPEG inclusion.",
- long_description=open('README.md').read(),
+ long_description=open("README.md").read(),
+ long_description_content_type="text/markdown",
license="LGPL",
keywords="jpeg pdf converter",
classifiers=[
- 'Development Status :: 5 - Production/Stable',
- 'Intended Audience :: Developers',
- 'Intended Audience :: Other Audience',
- 'Environment :: Console',
- 'Programming Language :: Python',
- 'Programming Language :: Python :: 2',
- 'Programming Language :: Python :: 2.7',
- 'Programming Language :: Python :: 3',
- 'Programming Language :: Python :: 3.5',
- 'Programming Language :: Python :: Implementation :: CPython',
+ "Development Status :: 5 - Production/Stable",
+ "Intended Audience :: Developers",
+ "Intended Audience :: Other Audience",
+ "Environment :: Console",
+ "Programming Language :: Python",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.5",
+ "Programming Language :: Python :: Implementation :: CPython",
"Programming Language :: Python :: Implementation :: PyPy",
- 'License :: OSI Approved :: GNU Lesser General Public License v3 '
- '(LGPLv3)',
- 'Natural Language :: English',
- 'Operating System :: OS Independent'],
- url='https://gitlab.mister-muffin.de/josch/img2pdf',
- download_url='https://gitlab.mister-muffin.de/josch/img2pdf/repository/'
- 'archive.tar.gz?ref=' + VERSION,
+ "License :: OSI Approved :: GNU Lesser General Public License v3 " "(LGPLv3)",
+ "Natural Language :: English",
+ "Operating System :: OS Independent",
+ ],
+ url="https://gitlab.mister-muffin.de/josch/img2pdf",
+ download_url="https://gitlab.mister-muffin.de/josch/img2pdf/repository/"
+ "archive.tar.gz?ref=" + VERSION,
package_dir={"": "src"},
- py_modules=['img2pdf', 'jp2'],
+ py_modules=["img2pdf", "jp2"],
include_package_data=True,
- test_suite='tests.test_suite',
zip_safe=True,
install_requires=INSTALL_REQUIRES,
- tests_requires=TESTS_REQUIRE,
extras_require={
- 'test': TESTS_REQUIRE,
+ "gui": ("tkinter"),
+ },
+ entry_points={
+ "setuptools.installation": ["eggsecutable = img2pdf:main"],
+ "console_scripts": ["img2pdf = img2pdf:main"],
+ "gui_scripts": ["img2pdf-gui = img2pdf:gui"],
},
- entry_points='''
- [console_scripts]
- img2pdf = img2pdf:main
- ''',
- )
+)
diff --git a/src/img2pdf.egg-info/PKG-INFO b/src/img2pdf.egg-info/PKG-INFO
index 7553591..cc05494 100644
--- a/src/img2pdf.egg-info/PKG-INFO
+++ b/src/img2pdf.egg-info/PKG-INFO
@@ -1,240 +1,18 @@
Metadata-Version: 2.1
Name: img2pdf
-Version: 0.3.3
+Version: 0.5.1
Summary: Convert images to PDF via direct JPEG inclusion.
Home-page: https://gitlab.mister-muffin.de/josch/img2pdf
-Author: Johannes 'josch' Schauer
+Download-URL: https://gitlab.mister-muffin.de/josch/img2pdf/repository/archive.tar.gz?ref=0.5.1
+Author: Johannes Schauer Marin Rodrigues
Author-email: josch@mister-muffin.de
License: LGPL
-Download-URL: https://gitlab.mister-muffin.de/josch/img2pdf/repository/archive.tar.gz?ref=0.3.3
-Description: img2pdf
- =======
-
- Lossless conversion of raster images to PDF. You should use img2pdf if your
- priorities are (in this order):
-
- 1. **always lossless**: the image embedded in the PDF will always have the
- exact same color information for every pixel as the input
- 2. **small**: if possible, the difference in filesize between the input image
- and the output PDF will only be the overhead of the PDF container itself
- 3. **fast**: if possible, the input image is just pasted into the PDF document
- as-is without any CPU hungry re-encoding of the pixel data
-
- Conventional conversion software (like ImageMagick) would either:
-
- 1. not be lossless because lossy re-encoding to JPEG
- 2. not be small because using wasteful flate encoding of raw pixel data
- 3. not be fast because input data gets re-encoded
-
- Another advantage of not having to re-encode the input (in most common
- situations) is, that img2pdf is able to handle much larger input than other
- software, because the raw pixel data never has to be loaded into memory.
-
- The following table shows how img2pdf handles different input depending on the
- input file format and image color space.
-
- | Format | Colorspace | Result |
- | -------------------- | ------------------------------ | ------------- |
- | JPEG | any | direct |
- | JPEG2000 | any | direct |
- | PNG (non-interlaced) | any | direct |
- | TIFF (CCITT Group 4) | monochrome | direct |
- | any | any except CMYK and monochrome | PNG Paeth |
- | any | monochrome | CCITT Group 4 |
- | any | CMYK | flate |
-
- For JPEG, JPEG2000, non-interlaced PNG and TIFF images with CCITT Group 4
- encoded data, img2pdf directly embeds the image data into the PDF without
- re-encoding it. It thus treats the PDF format merely as a container format for
- the image data. In these cases, img2pdf only increases the filesize by the size
- of the PDF container (typically around 500 to 700 bytes). Since data is only
- copied and not re-encoded, img2pdf is also typically faster than other
- solutions for these input formats.
-
- For all other input types, img2pdf first has to transform the pixel data to
- make it compatible with PDF. In most cases, the PNG Paeth filter is applied to
- the pixel data. For monochrome input, CCITT Group 4 is used instead. Only for
- CMYK input no filter is applied before finally applying flate compression.
-
- Usage
- -----
-
- The images must be provided as files because img2pdf needs to seek in the file
- descriptor.
-
- If no output file is specified with the `-o`/`--output` option, output will be
- done to stdout. A typical invocation is:
-
- $ img2pdf img1.png img2.jpg -o out.pdf
-
- The detailed documentation can be accessed by running:
-
- $ img2pdf --help
-
- Bugs
- ----
-
- - If you find a JPEG, JPEG2000, PNG or CCITT Group 4 encoded TIFF file that,
- when embedded into the PDF cannot be read by the Adobe Acrobat Reader,
- please contact me.
-
- - I have not yet figured out how to determine the colorspace of JPEG2000
- files. Therefore JPEG2000 files use DeviceRGB by default. For JPEG2000
- files with other colorspaces, you must explicitly specify it using the
- `--colorspace` option.
-
- - Input images with alpha channels are not allowed. PDF doesn't support alpha
- channels in images and thus, the alpha channel of the input would have to be
- discarded. But img2pdf will always be lossless and thus, input images must
- not carry transparency information.
-
- - img2pdf uses PIL (or Pillow) to obtain image meta data and to convert the
- input if necessary. To prevent decompression bomb denial of service attacks,
- Pillow limits the maximum number of pixels an input image is allowed to
- have. If you are sure that you know what you are doing, then you can disable
- this safeguard by passing the `--pillow-limit-break` option to img2pdf. This
- allows one to process even very large input images.
-
- Installation
- ------------
-
- On a Debian- and Ubuntu-based systems, img2pdf can be installed from the
- official repositories:
-
- $ apt install img2pdf
-
- If you want to install it using pip, you can run:
-
- $ pip3 install img2pdf
-
- If you prefer to install from source code use:
-
- $ cd img2pdf/
- $ pip3 install .
-
- To test the console script without installing the package on your system,
- use virtualenv:
-
- $ cd img2pdf/
- $ virtualenv ve
- $ ve/bin/pip3 install .
-
- You can then test the converter using:
-
- $ ve/bin/img2pdf -o test.pdf src/tests/test.jpg
-
- The package can also be used as a library:
-
- import img2pdf
-
- # opening from filename
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert('test.jpg'))
-
- # opening from file handle
- with open("name.pdf","wb") as f1, open("test.jpg") as f2:
- f1.write(img2pdf.convert(f2))
-
- # using in-memory image data
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert("\x89PNG...")
-
- # multiple inputs (variant 1)
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert("test1.jpg", "test2.png"))
-
- # multiple inputs (variant 2)
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert(["test1.jpg", "test2.png"]))
-
- # writing to file descriptor
- with open("name.pdf","wb") as f1, open("test.jpg") as f2:
- img2pdf.convert(f2, outputstream=f1)
-
- # specify paper size (A4)
- a4inpt = (img2pdf.mm_to_pt(210),img2pdf.mm_to_pt(297))
- layout_fun = img2pdf.get_layout_fun(a4inpt)
- with open("name.pdf","wb") as f:
- f.write(img2pdf.convert('test.jpg', layout_fun=layout_fun))
-
- Comparison to ImageMagick
- -------------------------
-
- Create a large test image:
-
- $ convert logo: -resize 8000x original.jpg
-
- Convert it into PDF using ImageMagick and img2pdf:
-
- $ time img2pdf original.jpg -o img2pdf.pdf
- $ time convert original.jpg imagemagick.pdf
-
- Notice how ImageMagick took an order of magnitude longer to do the conversion
- than img2pdf. It also used twice the memory.
-
- Now extract the image data from both PDF documents and compare it to the
- original:
-
- $ pdfimages -all img2pdf.pdf tmp
- $ compare -metric AE original.jpg tmp-000.jpg null:
- 0
- $ pdfimages -all imagemagick.pdf tmp
- $ compare -metric AE original.jpg tmp-000.jpg null:
- 118716
-
- To get lossless output with ImageMagick we can use Zip compression but that
- unnecessarily increases the size of the output:
-
- $ convert original.jpg -compress Zip imagemagick.pdf
- $ pdfimages -all imagemagick.pdf tmp
- $ compare -metric AE original.jpg tmp-000.png null:
- 0
- $ stat --format="%s %n" original.jpg img2pdf.pdf imagemagick.pdf
- 1535837 original.jpg
- 1536683 img2pdf.pdf
- 9397809 imagemagick.pdf
-
- Comparison to pdfLaTeX
- ----------------------
-
- pdfLaTeX performs a lossless conversion from included images to PDF by default.
- If the input is a JPEG, then it simply embeds the JPEG into the PDF in the same
- way as img2pdf does it. But for other image formats it uses flate compression
- of the plain pixel data and thus needlessly increases the output file size:
-
- $ convert logo: -resize 8000x original.png
- $ cat << END > pdflatex.tex
- \documentclass{article}
- \usepackage{graphicx}
- \begin{document}
- \includegraphics{original.png}
- \end{document}
- END
- $ pdflatex pdflatex.tex
- $ stat --format="%s %n" original.png pdflatex.pdf
- 4500182 original.png
- 9318120 pdflatex.pdf
-
- Comparison to Tesseract OCR
- ---------------------------
-
- Tesseract OCR comes closest to the functionality img2pdf provides. It is able
- to convert JPEG and PNG input to PDF without needlessly increasing the filesize
- and is at the same time lossless. So if your input is JPEG and PNG images, then
- you should safely be able to use Tesseract instead of img2pdf. For other input,
- Tesseract might not do a lossless conversion. For example it converts CMYK
- input to RGB and removes the alpha channel from images with transparency. For
- multipage TIFF or animated GIF, it will only convert the first frame.
-
Keywords: jpeg pdf converter
-Platform: UNKNOWN
Classifier: Development Status :: 5 - Production/Stable
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Other Audience
Classifier: Environment :: Console
Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2
-Classifier: Programming Language :: Python :: 2.7
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.5
Classifier: Programming Language :: Python :: Implementation :: CPython
@@ -242,4 +20,328 @@ Classifier: Programming Language :: Python :: Implementation :: PyPy
Classifier: License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)
Classifier: Natural Language :: English
Classifier: Operating System :: OS Independent
-Provides-Extra: test
+Description-Content-Type: text/markdown
+Provides-Extra: gui
+License-File: LICENSE
+
+[![Travis Status](https://travis-ci.com/josch/img2pdf.svg?branch=main)](https://app.travis-ci.com/josch/img2pdf)
+[![Appveyor Status](https://ci.appveyor.com/api/projects/status/2kws3wkqvi526llj/branch/main?svg=true)](https://ci.appveyor.com/project/josch/img2pdf/branch/main)
+
+img2pdf
+=======
+
+Lossless conversion of raster images to PDF. You should use img2pdf if your
+priorities are (in this order):
+
+ 1. **always lossless**: the image embedded in the PDF will always have the
+ exact same color information for every pixel as the input
+ 2. **small**: if possible, the difference in filesize between the input image
+ and the output PDF will only be the overhead of the PDF container itself
+ 3. **fast**: if possible, the input image is just pasted into the PDF document
+ as-is without any CPU hungry re-encoding of the pixel data
+
+Conventional conversion software (like ImageMagick) would either:
+
+ 1. not be lossless because lossy re-encoding to JPEG
+ 2. not be small because using wasteful flate encoding of raw pixel data
+ 3. not be fast because input data gets re-encoded
+
+Another advantage of not having to re-encode the input (in most common
+situations) is, that img2pdf is able to handle much larger input than other
+software, because the raw pixel data never has to be loaded into memory.
+
+The following table shows how img2pdf handles different input depending on the
+input file format and image color space.
+
+| Format | Colorspace | Result |
+| ------------------------------------- | ------------------------------ | ------------- |
+| JPEG | any | direct |
+| JPEG2000 | any | direct |
+| PNG (non-interlaced, no transparency) | any | direct |
+| TIFF (CCITT Group 4) | monochrome | direct |
+| any | any except CMYK and monochrome | PNG Paeth |
+| any | monochrome | CCITT Group 4 |
+| any | CMYK | flate |
+
+For JPEG, JPEG2000, non-interlaced PNG and TIFF images with CCITT Group 4
+encoded data, img2pdf directly embeds the image data into the PDF without
+re-encoding it. It thus treats the PDF format merely as a container format for
+the image data. In these cases, img2pdf only increases the filesize by the size
+of the PDF container (typically around 500 to 700 bytes). Since data is only
+copied and not re-encoded, img2pdf is also typically faster than other
+solutions for these input formats.
+
+For all other input types, img2pdf first has to transform the pixel data to
+make it compatible with PDF. In most cases, the PNG Paeth filter is applied to
+the pixel data. For monochrome input, CCITT Group 4 is used instead. Only for
+CMYK input no filter is applied before finally applying flate compression.
+
+Usage
+-----
+
+The images must be provided as files because img2pdf needs to seek in the file
+descriptor.
+
+If no output file is specified with the `-o`/`--output` option, output will be
+done to stdout. A typical invocation is:
+
+ $ img2pdf img1.png img2.jpg -o out.pdf
+
+The detailed documentation can be accessed by running:
+
+ $ img2pdf --help
+
+Bugs
+----
+
+ - If you find a JPEG, JPEG2000, PNG or CCITT Group 4 encoded TIFF file that,
+ when embedded into the PDF cannot be read by the Adobe Acrobat Reader,
+ please contact me.
+
+ - An error is produced if the input image is broken. This commonly happens if
+ the input image has an invalid EXIF Orientation value of zero. Even though
+ only nine different values from 1 to 9 are permitted, Anroid phones and
+ Canon DSLR cameras produce JPEG images with the invalid value of zero.
+ Either fix your input images with `exiftool` or similar software before
+ passing the JPEG to `img2pdf` or run `img2pdf` with `--rotation=ifvalid`
+ (if you run img2pdf from the commandline) or by passing
+ `rotation=img2pdf.Rotation.ifvalid` as an argument to `convert()` when using
+ img2pdf as a library.
+
+ - img2pdf uses PIL (or Pillow) to obtain image meta data and to convert the
+ input if necessary. To prevent decompression bomb denial of service attacks,
+ Pillow limits the maximum number of pixels an input image is allowed to
+ have. If you are sure that you know what you are doing, then you can disable
+ this safeguard by passing the `--pillow-limit-break` option to img2pdf. This
+ allows one to process even very large input images.
+
+Installation
+------------
+
+On a Debian- and Ubuntu-based systems, img2pdf can be installed from the
+official repositories:
+
+ $ apt install img2pdf
+
+If you want to install it using pip, you can run:
+
+ $ pip3 install img2pdf
+
+If you prefer to install from source code use:
+
+ $ cd img2pdf/
+ $ pip3 install .
+
+To test the console script without installing the package on your system,
+use virtualenv:
+
+ $ cd img2pdf/
+ $ virtualenv ve
+ $ ve/bin/pip3 install .
+
+You can then test the converter using:
+
+ $ ve/bin/img2pdf -o test.pdf src/tests/test.jpg
+
+If you don't want to setup Python on Windows, then head to the
+[releases](/josch/img2pdf/releases) section and download the latest
+`img2pdf.exe`.
+
+GUI
+---
+
+There exists an experimental GUI with all settings currently disabled. You can
+directly convert images to PDF but you cannot set any options via the GUI yet.
+If you are interested in adding more features to the PDF, please submit a merge
+request. The GUI is based on tkinter and works on Linux, Windows and MacOS.
+
+![](screenshot.png)
+
+Library
+-------
+
+The package can also be used as a library:
+
+ import img2pdf
+
+ # opening from filename
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg'))
+
+ # opening from file handle
+ with open("name.pdf","wb") as f1, open("test.jpg") as f2:
+ f1.write(img2pdf.convert(f2))
+
+ # opening using pathlib
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(pathlib.Path('test.jpg')))
+
+ # using in-memory image data
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert("\x89PNG...")
+
+ # multiple inputs (variant 1)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert("test1.jpg", "test2.png"))
+
+ # multiple inputs (variant 2)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(["test1.jpg", "test2.png"]))
+
+ # convert all files ending in .jpg inside a directory
+ dirname = "/path/to/images"
+ imgs = []
+ for fname in os.listdir(dirname):
+ if not fname.endswith(".jpg"):
+ continue
+ path = os.path.join(dirname, fname)
+ if os.path.isdir(path):
+ continue
+ imgs.append(path)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(imgs))
+
+ # convert all files ending in .jpg in a directory and its subdirectories
+ dirname = "/path/to/images"
+ imgs = []
+ for r, _, f in os.walk(dirname):
+ for fname in f:
+ if not fname.endswith(".jpg"):
+ continue
+ imgs.append(os.path.join(r, fname))
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(imgs))
+
+
+ # convert all files matching a glob
+ import glob
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(glob.glob("/path/to/*.jpg")))
+
+ # convert all files matching a glob using pathlib.Path
+ from pathlib import Path
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert(*Path("/path").glob("**/*.jpg")))
+
+ # ignore invalid rotation values in the input images
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg'), rotation=img2pdf.Rotation.ifvalid)
+
+ # writing to file descriptor
+ with open("name.pdf","wb") as f1, open("test.jpg") as f2:
+ img2pdf.convert(f2, outputstream=f1)
+
+ # specify paper size (A4)
+ a4inpt = (img2pdf.mm_to_pt(210),img2pdf.mm_to_pt(297))
+ layout_fun = img2pdf.get_layout_fun(a4inpt)
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg', layout_fun=layout_fun))
+
+ # use a fixed dpi of 300 instead of reading it from the image
+ dpix = dpiy = 300
+ layout_fun = img2pdf.get_fixed_dpi_layout_fun((dpix, dpiy))
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg', layout_fun=layout_fun))
+
+ # create a PDF/A-1b compliant document by passing an ICC profile
+ with open("name.pdf","wb") as f:
+ f.write(img2pdf.convert('test.jpg', pdfa="/usr/share/color/icc/sRGB.icc"))
+
+Comparison to ImageMagick
+-------------------------
+
+Create a large test image:
+
+ $ convert logo: -resize 8000x original.jpg
+
+Convert it into PDF using ImageMagick and img2pdf:
+
+ $ time img2pdf original.jpg -o img2pdf.pdf
+ $ time convert original.jpg imagemagick.pdf
+
+Notice how ImageMagick took an order of magnitude longer to do the conversion
+than img2pdf. It also used twice the memory.
+
+Now extract the image data from both PDF documents and compare it to the
+original:
+
+ $ pdfimages -all img2pdf.pdf tmp
+ $ compare -metric AE original.jpg tmp-000.jpg null:
+ 0
+ $ pdfimages -all imagemagick.pdf tmp
+ $ compare -metric AE original.jpg tmp-000.jpg null:
+ 118716
+
+To get lossless output with ImageMagick we can use Zip compression but that
+unnecessarily increases the size of the output:
+
+ $ convert original.jpg -compress Zip imagemagick.pdf
+ $ pdfimages -all imagemagick.pdf tmp
+ $ compare -metric AE original.jpg tmp-000.png null:
+ 0
+ $ stat --format="%s %n" original.jpg img2pdf.pdf imagemagick.pdf
+ 1535837 original.jpg
+ 1536683 img2pdf.pdf
+ 9397809 imagemagick.pdf
+
+Comparison to pdfLaTeX
+----------------------
+
+pdfLaTeX performs a lossless conversion from included images to PDF by default.
+If the input is a JPEG, then it simply embeds the JPEG into the PDF in the same
+way as img2pdf does it. But for other image formats it uses flate compression
+of the plain pixel data and thus needlessly increases the output file size:
+
+ $ convert logo: -resize 8000x original.png
+ $ cat << END > pdflatex.tex
+ \documentclass{article}
+ \usepackage{graphicx}
+ \begin{document}
+ \includegraphics{original.png}
+ \end{document}
+ END
+ $ pdflatex pdflatex.tex
+ $ stat --format="%s %n" original.png pdflatex.pdf
+ 4500182 original.png
+ 9318120 pdflatex.pdf
+
+Comparison to podofoimg2pdf
+---------------------------
+
+Like pdfLaTeX, podofoimg2pdf is able to perform a lossless conversion from JPEG
+to PDF by plainly embedding the JPEG data into the pdf container. But just like
+pdfLaTeX it uses flate compression for all other file formats, thus sometimes
+resulting in larger files than necessary.
+
+ $ convert logo: -resize 8000x original.png
+ $ podofoimg2pdf out.pdf original.png
+ stat --format="%s %n" original.png out.pdf
+ 4500181 original.png
+ 9335629 out.pdf
+
+It also only supports JPEG, PNG and TIF as input and lacks many of the
+convenience features of img2pdf like page sizes, borders, rotation and
+metadata.
+
+Comparison to Tesseract OCR
+---------------------------
+
+Tesseract OCR comes closest to the functionality img2pdf provides. It is able
+to convert JPEG and PNG input to PDF without needlessly increasing the filesize
+and is at the same time lossless. So if your input is JPEG and PNG images, then
+you should safely be able to use Tesseract instead of img2pdf. For other input,
+Tesseract might not do a lossless conversion. For example it converts CMYK
+input to RGB and removes the alpha channel from images with transparency. For
+multipage TIFF or animated GIF, it will only convert the first frame.
+
+Comparison to econvert from ExactImage
+--------------------------------------
+
+Like pdflatex and podofoimg2pf, econvert is able to embed JPEG images into PDF
+directly without re-encoding but when given other file formats, it stores them
+just using flate compressen, which unnecessarily increases the filesize.
+Furthermore, it throws an error with CMYK TIF input. It also doesn't store CMYK
+jpeg files as CMYK but converts them to RGB, so it's not lossless. When trying
+to feed it 16bit files, it errors out with Unhandled bps/spp combination. It
+also seems to choose JPEG encoding when using it on some file types (like
+palette images) making it again not lossless for that input as well.
diff --git a/src/img2pdf.egg-info/SOURCES.txt b/src/img2pdf.egg-info/SOURCES.txt
index 6fa068a..5648504 100644
--- a/src/img2pdf.egg-info/SOURCES.txt
+++ b/src/img2pdf.egg-info/SOURCES.txt
@@ -2,21 +2,18 @@ CHANGES.rst
LICENSE
MANIFEST.in
README.md
-setup.cfg
setup.py
-test.sh
test_comp.sh
src/img2pdf.py
+src/img2pdf_test.py
src/jp2.py
src/img2pdf.egg-info/PKG-INFO
src/img2pdf.egg-info/SOURCES.txt
src/img2pdf.egg-info/dependency_links.txt
src/img2pdf.egg-info/entry_points.txt
-src/img2pdf.egg-info/pbr.json
src/img2pdf.egg-info/requires.txt
src/img2pdf.egg-info/top_level.txt
src/img2pdf.egg-info/zip-safe
-src/tests/__init__.py
src/tests/input/CMYK.jpg
src/tests/input/CMYK.tif
src/tests/input/animation.gif
diff --git a/src/img2pdf.egg-info/entry_points.txt b/src/img2pdf.egg-info/entry_points.txt
index 59301dc..e9e721b 100644
--- a/src/img2pdf.egg-info/entry_points.txt
+++ b/src/img2pdf.egg-info/entry_points.txt
@@ -1,4 +1,8 @@
+[console_scripts]
+img2pdf = img2pdf:main
- [console_scripts]
- img2pdf = img2pdf:main
- \ No newline at end of file
+[gui_scripts]
+img2pdf-gui = img2pdf:gui
+
+[setuptools.installation]
+eggsecutable = img2pdf:main
diff --git a/src/img2pdf.egg-info/pbr.json b/src/img2pdf.egg-info/pbr.json
deleted file mode 100644
index bc27bf9..0000000
--- a/src/img2pdf.egg-info/pbr.json
+++ /dev/null
@@ -1 +0,0 @@
-{"is_release": false, "git_version": "d78b2cb"} \ No newline at end of file
diff --git a/src/img2pdf.egg-info/requires.txt b/src/img2pdf.egg-info/requires.txt
index 3a24589..b676918 100644
--- a/src/img2pdf.egg-info/requires.txt
+++ b/src/img2pdf.egg-info/requires.txt
@@ -1,4 +1,5 @@
Pillow
+pikepdf
-[test]
-pdfrw
+[gui]
+tkinter
diff --git a/src/img2pdf.py b/src/img2pdf.py
index 27e5b8c..56db773 100755
--- a/src/img2pdf.py
+++ b/src/img2pdf.py
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
-# Copyright (C) 2012-2014 Johannes 'josch' Schauer <j.schauer at email.de>
+# Copyright (C) 2012-2021 Johannes Schauer Marin Rodrigues <josch@mister-muffin.de>
#
# This program is free software: you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
@@ -22,20 +22,47 @@ import sys
import os
import zlib
import argparse
-from PIL import Image, TiffImagePlugin
+from PIL import Image, TiffImagePlugin, GifImagePlugin, ImageCms
+
+if hasattr(GifImagePlugin, "LoadingStrategy"):
+ # Pillow 9.0.0 started emitting all frames but the first as RGB instead of
+ # P to make sure that more than 256 colors can be represented. But palette
+ # images compress far better than RGB images in PDF so we instruct Pillow
+ # to only emit RGB frames if the palette differs and return P otherwise.
+ # This works since Pillow 9.1.0.
+ GifImagePlugin.LOADING_STRATEGY = (
+ GifImagePlugin.LoadingStrategy.RGB_AFTER_DIFFERENT_PALETTE_ONLY
+ )
# TiffImagePlugin.DEBUG = True
from PIL.ExifTags import TAGS
-from datetime import datetime
-from jp2 import parsejp2
+from datetime import datetime, timezone
+import jp2
from enum import Enum
from io import BytesIO
import logging
import struct
-
-PY3 = sys.version_info[0] >= 3
-
-__version__ = "0.3.3"
+import platform
+import hashlib
+from itertools import chain
+import re
+import io
+
+logger = logging.getLogger(__name__)
+
+have_pdfrw = True
+try:
+ import pdfrw
+except ImportError:
+ have_pdfrw = False
+
+have_pikepdf = True
+try:
+ import pikepdf
+except ImportError:
+ have_pikepdf = False
+
+__version__ = "0.5.1"
default_dpi = 96.0
papersizes = {
"letter": "8.5inx11in",
@@ -46,6 +73,20 @@ papersizes = {
"a4": "210mmx297mm",
"a5": "148mmx210mm",
"a6": "105mmx148mm",
+ "b0": "1000mmx1414mm",
+ "b1": "707mmx1000mm",
+ "b2": "500mmx707mm",
+ "b3": "353mmx500mm",
+ "b4": "250mmx353mm",
+ "b5": "176mmx250mm",
+ "b6": "125mmx176mm",
+ "jb0": "1030mmx1456mm",
+ "jb1": "728mmx1030mm",
+ "jb2": "515mmx728mm",
+ "jb3": "364mmx515mm",
+ "jb4": "257mmx364mm",
+ "jb5": "182mmx257mm",
+ "jb6": "128mmx182mm",
"legal": "8.5inx14in",
"tabloid": "11inx17in",
}
@@ -58,22 +99,44 @@ papernames = {
"a4": "A4",
"a5": "A5",
"a6": "A6",
+ "b0": "B0",
+ "b1": "B1",
+ "b2": "B2",
+ "b3": "B3",
+ "b4": "B4",
+ "b5": "B5",
+ "b6": "B6",
+ "jb0": "JB0",
+ "jb1": "JB1",
+ "jb2": "JB2",
+ "jb3": "JB3",
+ "jb4": "JB4",
+ "jb5": "JB5",
+ "jb6": "JB6",
"legal": "Legal",
"tabloid": "Tabloid",
}
+Engine = Enum("Engine", "internal pdfrw pikepdf")
+
+Rotation = Enum("Rotation", "auto none ifvalid 0 90 180 270")
FitMode = Enum("FitMode", "into fill exact shrink enlarge")
PageOrientation = Enum("PageOrientation", "portrait landscape")
-Colorspace = Enum("Colorspace", "RGB L 1 CMYK CMYK;I RGBA P other")
+Colorspace = Enum("Colorspace", "RGB RGBA L LA 1 CMYK CMYK;I P PA other")
-ImageFormat = Enum("ImageFormat", "JPEG JPEG2000 CCITTGroup4 PNG TIFF other")
+ImageFormat = Enum(
+ "ImageFormat", "JPEG JPEG2000 CCITTGroup4 PNG GIF TIFF MPO MIFF other"
+)
PageMode = Enum("PageMode", "none outlines thumbs")
-PageLayout = Enum("PageLayout", "single onecolumn twocolumnright twocolumnleft")
+PageLayout = Enum(
+ "PageLayout",
+ "single onecolumn twocolumnright twocolumnleft twopageright twopageleft",
+)
Magnification = Enum("Magnification", "fit fith fitbh")
@@ -363,6 +426,36 @@ class PdfTooLargeError(Exception):
pass
+class AlphaChannelError(Exception):
+ pass
+
+
+class ExifOrientationError(Exception):
+ pass
+
+
+# temporary change the attribute of an object using a context manager
+class temp_attr:
+ def __init__(self, obj, field, value):
+ self.obj = obj
+ self.field = field
+ self.value = value
+
+ def __enter__(self):
+ self.exists = False
+ if hasattr(self.obj, self.field):
+ self.exists = True
+ self.old_value = getattr(self.obj, self.field)
+ logger.debug(f"setting {self.obj}.{self.field} = {self.value}")
+ setattr(self.obj, self.field, self.value)
+
+ def __exit__(self, exctype, excinst, exctb):
+ if self.exists:
+ setattr(self.obj, self.field, self.old_value)
+ else:
+ delattr(self.obj, self.field)
+
+
# without pdfrw this function is a no-op
def my_convert_load(string):
return string
@@ -447,6 +540,9 @@ class MyPdfDict(object):
def __getitem__(self, key):
return self.content[key]
+ def __contains__(self, key):
+ return key in self.content
+
class MyPdfName:
def __getattr__(self, name):
@@ -466,13 +562,12 @@ class MyPdfArray(list):
class MyPdfWriter:
- def __init__(self, version="1.3"):
+ def __init__(self):
self.objects = []
# create an incomplete pages object so that a /Parent entry can be
# added to each page
self.pages = MyPdfDict(Type=MyPdfName.Pages, Kids=[], Count=0)
self.catalog = MyPdfDict(Pages=self.pages, Type=MyPdfName.Catalog)
- self.version = version # default pdf version 1.3
self.pagearray = []
def addobj(self, obj):
@@ -480,7 +575,7 @@ class MyPdfWriter:
obj.identifier = newid
self.objects.append(obj)
- def tostream(self, info, stream):
+ def tostream(self, info, stream, version="1.3", ident=None):
xreftable = list()
# justification of the random binary garbage in the header from
@@ -497,7 +592,7 @@ class MyPdfWriter:
#
# the choice of binary characters is arbitrary but those four seem to
# be used elsewhere.
- pdfheader = ("%%PDF-%s\n" % self.version).encode("ascii")
+ pdfheader = ("%%PDF-%s\n" % version).encode("ascii")
pdfheader += b"%\xe2\xe3\xcf\xd3\n"
stream.write(pdfheader)
@@ -537,10 +632,11 @@ class MyPdfWriter:
for x in xreftable:
stream.write(x)
stream.write(b"trailer\n")
- stream.write(
- parse({b"/Size": len(xreftable), b"/Info": info, b"/Root": self.catalog})
- + b"\n"
- )
+ trailer = {b"/Size": len(xreftable), b"/Info": info, b"/Root": self.catalog}
+ if ident is not None:
+ md5 = hashlib.md5(ident).hexdigest().encode("ascii")
+ trailer[b"/ID"] = b"[<%s><%s>]" % (md5, md5)
+ stream.write(parse(trailer) + b"\n")
stream.write(b"startxref\n")
stream.write(("%d\n" % xrefoffset).encode())
stream.write(b"%%EOF\n")
@@ -554,54 +650,32 @@ class MyPdfWriter:
self.addobj(page)
-if PY3:
-
- class MyPdfString:
- @classmethod
- def encode(cls, string, hextype=False):
- if hextype:
- return (
- b"< "
- + b" ".join(("%06x" % c).encode("ascii") for c in string)
- + b" >"
- )
- else:
- try:
- string = string.encode("ascii")
- except UnicodeEncodeError:
- string = b"\xfe\xff" + string.encode("utf-16-be")
- # We should probably encode more here because at least
- # ghostscript interpretes a carriage return byte (0x0D) as a
- # new line byte (0x0A)
- # PDF supports: \n, \r, \t, \b and \f
- string = string.replace(b"\\", b"\\\\")
- string = string.replace(b"(", b"\\(")
- string = string.replace(b")", b"\\)")
- return b"(" + string + b")"
-
-
-else:
-
- class MyPdfString(object):
- @classmethod
- def encode(cls, string, hextype=False):
- if hextype:
- return (
- b"< "
- + b" ".join(("%06x" % c).encode("ascii") for c in string)
- + b" >"
- )
- else:
- # This mimics exactely to what pdfrw does.
- string = string.replace(b"\\", b"\\\\")
- string = string.replace(b"(", b"\\(")
- string = string.replace(b")", b"\\)")
- return b"(" + string + b")"
+class MyPdfString:
+ @classmethod
+ def encode(cls, string, hextype=False):
+ if hextype:
+ return (
+ b"< " + b" ".join(("%06x" % c).encode("ascii") for c in string) + b" >"
+ )
+ else:
+ try:
+ string = string.encode("ascii")
+ except UnicodeEncodeError:
+ string = b"\xfe\xff" + string.encode("utf-16-be")
+ # We should probably encode more here because at least
+ # ghostscript interpretes a carriage return byte (0x0D) as a
+ # new line byte (0x0A)
+ # PDF supports: \n, \r, \t, \b and \f
+ string = string.replace(b"\\", b"\\\\")
+ string = string.replace(b"(", b"\\(")
+ string = string.replace(b")", b"\\)")
+ return b"(" + string + b")"
class pdfdoc(object):
def __init__(
self,
+ engine=Engine.internal,
version="1.3",
title=None,
author=None,
@@ -619,69 +693,103 @@ class pdfdoc(object):
fit_window=False,
center_window=False,
fullscreen=False,
- with_pdfrw=True,
+ pdfa=None,
):
- if with_pdfrw:
- try:
- from pdfrw import PdfWriter, PdfDict, PdfName, PdfString
-
- self.with_pdfrw = True
- except ImportError:
- PdfWriter = MyPdfWriter
- PdfDict = MyPdfDict
- PdfName = MyPdfName
- PdfString = MyPdfString
- self.with_pdfrw = False
- else:
+ if engine is None:
+ if have_pikepdf:
+ engine = Engine.pikepdf
+ elif have_pdfrw:
+ engine = Engine.pdfrw
+ else:
+ engine = Engine.internal
+
+ if engine == Engine.pikepdf:
+ PdfWriter = pikepdf.new
+ PdfDict = pikepdf.Dictionary
+ PdfName = pikepdf.Name
+ elif engine == Engine.pdfrw:
+ from pdfrw import PdfWriter, PdfDict, PdfName, PdfString
+ elif engine == Engine.internal:
PdfWriter = MyPdfWriter
PdfDict = MyPdfDict
PdfName = MyPdfName
PdfString = MyPdfString
- self.with_pdfrw = False
+ else:
+ raise ValueError("unknown engine: %s" % engine)
- now = datetime.now()
- self.info = PdfDict(indirect=True)
+ self.writer = PdfWriter()
+ if engine != Engine.pikepdf:
+ self.writer.docinfo = PdfDict(indirect=True)
def datetime_to_pdfdate(dt):
- return dt.strftime("%Y%m%d%H%M%SZ")
-
- if title is not None:
- self.info[PdfName.Title] = PdfString.encode(title)
- if author is not None:
- self.info[PdfName.Author] = PdfString.encode(author)
- if creator is not None:
- self.info[PdfName.Creator] = PdfString.encode(creator)
- if producer is not None and producer != "":
- self.info[PdfName.Producer] = PdfString.encode(producer)
- if creationdate is not None:
- self.info[PdfName.CreationDate] = PdfString.encode(
- "D:" + datetime_to_pdfdate(creationdate)
- )
- elif not nodate:
- self.info[PdfName.CreationDate] = PdfString.encode(
- "D:" + datetime_to_pdfdate(now)
- )
- if moddate is not None:
- self.info[PdfName.ModDate] = PdfString.encode(
- "D:" + datetime_to_pdfdate(moddate)
- )
- elif not nodate:
- self.info[PdfName.ModDate] = PdfString.encode(
- "D:" + datetime_to_pdfdate(now)
- )
- if subject is not None:
- self.info[PdfName.Subject] = PdfString.encode(subject)
+ return dt.astimezone(tz=timezone.utc).strftime("%Y%m%d%H%M%SZ")
+
+ for k in ["Title", "Author", "Creator", "Producer", "Subject"]:
+ v = locals()[k.lower()]
+ if v is None or v == "":
+ continue
+ if engine != Engine.pikepdf:
+ v = PdfString.encode(v)
+ self.writer.docinfo[getattr(PdfName, k)] = v
+
+ now = datetime.now().astimezone()
+ for k in ["CreationDate", "ModDate"]:
+ v = locals()[k.lower()]
+ if v is None and nodate:
+ continue
+ if v is None:
+ v = now
+ v = ("D:" + datetime_to_pdfdate(v)).encode("ascii")
+ if engine == Engine.internal:
+ v = b"(" + v + b")"
+ self.writer.docinfo[getattr(PdfName, k)] = v
if keywords is not None:
- self.info[PdfName.Keywords] = PdfString.encode(",".join(keywords))
+ if engine == Engine.pikepdf:
+ self.writer.docinfo[PdfName.Keywords] = ",".join(keywords)
+ else:
+ self.writer.docinfo[PdfName.Keywords] = PdfString.encode(
+ ",".join(keywords)
+ )
- self.writer = PdfWriter()
- self.writer.version = version
- # this is done because pdfrw adds info, catalog and pages as the first
- # three objects in this order
- if not self.with_pdfrw:
- self.writer.addobj(self.info)
- self.writer.addobj(self.writer.catalog)
- self.writer.addobj(self.writer.pages)
+ def datetime_to_xmpdate(dt):
+ return dt.astimezone(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ")
+
+ self.xmp = b"""<?xpacket begin='\xef\xbb\xbf' id='W5M0MpCehiHzreSzNTczkc9d'?>
+<x:xmpmeta xmlns:x='adobe:ns:meta/' x:xmptk='XMP toolkit 2.9.1-13, framework 1.6'>
+<rdf:RDF xmlns:rdf='http://www.w3.org/1999/02/22-rdf-syntax-ns#' xmlns:iX='http://ns.adobe.com/iX/1.0/'>
+ <rdf:Description rdf:about='' xmlns:pdf='http://ns.adobe.com/pdf/1.3/'%s/>
+ <rdf:Description rdf:about='' xmlns:xmp='http://ns.adobe.com/xap/1.0/'>
+ %s
+ %s
+ </rdf:Description>
+ <rdf:Description rdf:about='' xmlns:pdfaid='http://www.aiim.org/pdfa/ns/id/' pdfaid:part='1' pdfaid:conformance='B'/>
+</rdf:RDF>
+</x:xmpmeta>
+
+<?xpacket end='w'?>
+""" % (
+ b" pdf:Producer='%s'" % producer.encode("ascii")
+ if producer is not None
+ else b"",
+ b""
+ if creationdate is None and nodate
+ else b"<xmp:ModifyDate>%s</xmp:ModifyDate>"
+ % datetime_to_xmpdate(now if creationdate is None else creationdate).encode(
+ "ascii"
+ ),
+ b""
+ if moddate is None and nodate
+ else b"<xmp:CreateDate>%s</xmp:CreateDate>"
+ % datetime_to_xmpdate(now if moddate is None else moddate).encode("ascii"),
+ )
+
+ if engine != Engine.pikepdf:
+ # this is done because pdfrw adds info, catalog and pages as the first
+ # three objects in this order
+ if engine == Engine.internal:
+ self.writer.addobj(self.writer.docinfo)
+ self.writer.addobj(self.writer.catalog)
+ self.writer.addobj(self.writer.pages)
self.panes = panes
self.initial_page = initial_page
@@ -690,6 +798,9 @@ class pdfdoc(object):
self.fit_window = fit_window
self.center_window = center_window
self.fullscreen = fullscreen
+ self.engine = engine
+ self.output_version = version
+ self.pdfa = pdfa
def add_imagepage(
self,
@@ -698,6 +809,7 @@ class pdfdoc(object):
imgheightpx,
imgformat,
imgdata,
+ smaskdata,
imgwidthpdf,
imgheightpdf,
imgxpdf,
@@ -709,63 +821,125 @@ class pdfdoc(object):
inverted=False,
depth=0,
rotate=0,
+ cropborder=None,
+ bleedborder=None,
+ trimborder=None,
+ artborder=None,
+ iccp=None,
):
- if self.with_pdfrw:
+ assert (
+ color not in [Colorspace.RGBA, Colorspace.LA]
+ or (imgformat == ImageFormat.PNG and smaskdata is not None)
+ or imgformat == ImageFormat.JPEG2000
+ )
+
+ if self.engine == Engine.pikepdf:
+ PdfArray = pikepdf.Array
+ PdfDict = pikepdf.Dictionary
+ PdfName = pikepdf.Name
+ elif self.engine == Engine.pdfrw:
from pdfrw import PdfDict, PdfName, PdfObject, PdfString
from pdfrw.py23_diffs import convert_load
- else:
+ elif self.engine == Engine.internal:
PdfDict = MyPdfDict
PdfName = MyPdfName
PdfObject = MyPdfObject
PdfString = MyPdfString
convert_load = my_convert_load
+ else:
+ raise ValueError("unknown engine: %s" % self.engine)
+ TrueObject = True if self.engine == Engine.pikepdf else PdfObject("true")
+ FalseObject = False if self.engine == Engine.pikepdf else PdfObject("false")
- if color == Colorspace["1"] or color == Colorspace.L:
+ if color == Colorspace["1"] or color == Colorspace.L or color == Colorspace.LA:
colorspace = PdfName.DeviceGray
- elif color == Colorspace.RGB:
- colorspace = PdfName.DeviceRGB
+ elif color == Colorspace.RGB or color == Colorspace.RGBA:
+ if color == Colorspace.RGBA and imgformat == ImageFormat.JPEG2000:
+ # there is no DeviceRGBA and for JPXDecode it is okay to have
+ # no colorspace as the pdf reader is supposed to get this info
+ # from the jpeg2000 payload itself
+ colorspace = None
+ else:
+ colorspace = PdfName.DeviceRGB
elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]:
colorspace = PdfName.DeviceCMYK
elif color == Colorspace.P:
- if self.with_pdfrw:
+ if self.engine == Engine.pdfrw:
+ # https://github.com/pmaupin/pdfrw/issues/128
+ # https://github.com/pmaupin/pdfrw/issues/147
raise Exception(
"pdfrw does not support hex strings for "
"palette image input, re-run with "
- "--without-pdfrw"
+ "--engine=internal or --engine=pikepdf"
)
+ assert len(palette) % 3 == 0
colorspace = [
PdfName.Indexed,
PdfName.DeviceRGB,
- len(palette) - 1,
- PdfString.encode(palette, hextype=True),
+ (len(palette) // 3) - 1,
+ bytes(palette)
+ if self.engine == Engine.pikepdf
+ else PdfString.encode(
+ [
+ int.from_bytes(palette[i : i + 3], "big")
+ for i in range(0, len(palette), 3)
+ ],
+ hextype=True,
+ ),
]
else:
raise UnsupportedColorspaceError("unsupported color space: %s" % color.name)
+ if iccp is not None:
+ if self.engine == Engine.pikepdf:
+ iccpdict = self.writer.make_stream(iccp)
+ else:
+ iccpdict = PdfDict(stream=convert_load(iccp))
+ iccpdict[PdfName.Alternate] = colorspace
+ if (
+ color == Colorspace["1"]
+ or color == Colorspace.L
+ or color == Colorspace.LA
+ ):
+ iccpdict[PdfName.N] = 1
+ elif color == Colorspace.RGB or color == Colorspace.RGBA:
+ iccpdict[PdfName.N] = 3
+ elif color == Colorspace.CMYK or color == Colorspace["CMYK;I"]:
+ iccpdict[PdfName.N] = 4
+ elif color == Colorspace.P:
+ raise Exception("Cannot have Palette images with ICC profile")
+ colorspace = [PdfName.ICCBased, iccpdict]
+
# either embed the whole jpeg or deflate the bitmap representation
if imgformat is ImageFormat.JPEG:
ofilter = PdfName.DCTDecode
elif imgformat is ImageFormat.JPEG2000:
ofilter = PdfName.JPXDecode
- self.writer.version = "1.5" # jpeg2000 needs pdf 1.5
+ self.output_version = "1.5" # jpeg2000 needs pdf 1.5
elif imgformat is ImageFormat.CCITTGroup4:
ofilter = [PdfName.CCITTFaxDecode]
else:
ofilter = PdfName.FlateDecode
- image = PdfDict(stream=convert_load(imgdata))
+ if self.engine == Engine.pikepdf:
+ image = self.writer.make_stream(imgdata)
+ else:
+ image = PdfDict(stream=convert_load(imgdata))
image[PdfName.Type] = PdfName.XObject
image[PdfName.Subtype] = PdfName.Image
image[PdfName.Filter] = ofilter
image[PdfName.Width] = imgwidthpx
image[PdfName.Height] = imgheightpx
- image[PdfName.ColorSpace] = colorspace
+ if colorspace is not None:
+ image[PdfName.ColorSpace] = colorspace
image[PdfName.BitsPerComponent] = depth
+ smask = None
+
if color == Colorspace["CMYK;I"]:
# Inverts all four channels
- image[PdfName.Decode] = [1.0, 0.0, 1.0, 0.0, 1.0, 0.0, 1.0, 0.0]
+ image[PdfName.Decode] = [1, 0, 1, 0, 1, 0, 1, 0]
if imgformat is ImageFormat.CCITTGroup4:
decodeparms = PdfDict()
@@ -773,16 +947,42 @@ class pdfdoc(object):
# encoding. We set it to -1 because we want Group 4 encoding.
decodeparms[PdfName.K] = -1
if inverted:
- decodeparms[PdfName.BlackIs1] = PdfObject("false")
+ decodeparms[PdfName.BlackIs1] = FalseObject
else:
- decodeparms[PdfName.BlackIs1] = PdfObject("true")
+ decodeparms[PdfName.BlackIs1] = TrueObject
decodeparms[PdfName.Columns] = imgwidthpx
decodeparms[PdfName.Rows] = imgheightpx
image[PdfName.DecodeParms] = [decodeparms]
elif imgformat is ImageFormat.PNG:
+ if smaskdata is not None:
+ if self.engine == Engine.pikepdf:
+ smask = self.writer.make_stream(smaskdata)
+ else:
+ smask = PdfDict(stream=convert_load(smaskdata))
+ smask[PdfName.Type] = PdfName.XObject
+ smask[PdfName.Subtype] = PdfName.Image
+ smask[PdfName.Filter] = PdfName.FlateDecode
+ smask[PdfName.Width] = imgwidthpx
+ smask[PdfName.Height] = imgheightpx
+ smask[PdfName.ColorSpace] = PdfName.DeviceGray
+ smask[PdfName.BitsPerComponent] = depth
+
+ decodeparms = PdfDict()
+ decodeparms[PdfName.Predictor] = 15
+ decodeparms[PdfName.Colors] = 1
+ decodeparms[PdfName.Columns] = imgwidthpx
+ decodeparms[PdfName.BitsPerComponent] = depth
+ smask[PdfName.DecodeParms] = decodeparms
+
+ image[PdfName.SMask] = smask
+
+ # /SMask requires PDF 1.4
+ if self.output_version < "1.4":
+ self.output_version = "1.4"
+
decodeparms = PdfDict()
decodeparms[PdfName.Predictor] = 15
- if color in [Colorspace.P, Colorspace["1"], Colorspace.L]:
+ if color in [Colorspace.P, Colorspace["1"], Colorspace.L, Colorspace.LA]:
decodeparms[PdfName.Colors] = 1
else:
decodeparms[PdfName.Colors] = 3
@@ -795,27 +995,80 @@ class pdfdoc(object):
% (imgwidthpdf, imgheightpdf, imgxpdf, imgypdf)
).encode("ascii")
- content = PdfDict(stream=convert_load(text))
+ if self.engine == Engine.pikepdf:
+ content = self.writer.make_stream(text)
+ else:
+ content = PdfDict(stream=convert_load(text))
resources = PdfDict(XObject=PdfDict(Im0=image))
- page = PdfDict(indirect=True)
- page[PdfName.Type] = PdfName.Page
- page[PdfName.MediaBox] = [0, 0, pagewidth, pageheight]
+ if self.engine == Engine.pikepdf:
+ page = self.writer.add_blank_page(page_size=(pagewidth, pageheight))
+ else:
+ page = PdfDict(indirect=True)
+ page[PdfName.Type] = PdfName.Page
+ page[PdfName.MediaBox] = [0, 0, pagewidth, pageheight]
+ # 14.11.2 Page Boundaries
+ # ...
+ # The crop, bleed, trim, and art boxes shall not ordinarily extend
+ # beyond the boundaries of the media box. If they do, they are
+ # effectively reduced to their intersection with the media box.
+ if cropborder is not None:
+ page[PdfName.CropBox] = [
+ cropborder[1],
+ cropborder[0],
+ pagewidth - cropborder[1],
+ pageheight - cropborder[0],
+ ]
+ if bleedborder is None:
+ if PdfName.CropBox in page:
+ page[PdfName.BleedBox] = page[PdfName.CropBox]
+ else:
+ page[PdfName.BleedBox] = [
+ bleedborder[1],
+ bleedborder[0],
+ pagewidth - bleedborder[1],
+ pageheight - bleedborder[0],
+ ]
+ if trimborder is None:
+ if PdfName.CropBox in page:
+ page[PdfName.TrimBox] = page[PdfName.CropBox]
+ else:
+ page[PdfName.TrimBox] = [
+ trimborder[1],
+ trimborder[0],
+ pagewidth - trimborder[1],
+ pageheight - trimborder[0],
+ ]
+ if artborder is None:
+ if PdfName.CropBox in page:
+ page[PdfName.ArtBox] = page[PdfName.CropBox]
+ else:
+ page[PdfName.ArtBox] = [
+ artborder[1],
+ artborder[0],
+ pagewidth - artborder[1],
+ pageheight - artborder[0],
+ ]
page[PdfName.Resources] = resources
page[PdfName.Contents] = content
if rotate != 0:
page[PdfName.Rotate] = rotate
if userunit is not None:
# /UserUnit requires PDF 1.6
- if self.writer.version < "1.6":
- self.writer.version = "1.6"
+ if self.output_version < "1.6":
+ self.output_version = "1.6"
page[PdfName.UserUnit] = userunit
- self.writer.addpage(page)
+ if self.engine != Engine.pikepdf:
+ self.writer.addpage(page)
- if not self.with_pdfrw:
- self.writer.addobj(content)
- self.writer.addobj(image)
+ if self.engine == Engine.internal:
+ self.writer.addobj(content)
+ self.writer.addobj(image)
+ if smask is not None:
+ self.writer.addobj(smask)
+ if iccp is not None:
+ self.writer.addobj(iccpdict)
def tostring(self):
stream = BytesIO()
@@ -823,15 +1076,23 @@ class pdfdoc(object):
return stream.getvalue()
def tostream(self, outputstream):
- if self.with_pdfrw:
+ if self.engine == Engine.pikepdf:
+ PdfArray = pikepdf.Array
+ PdfDict = pikepdf.Dictionary
+ PdfName = pikepdf.Name
+ elif self.engine == Engine.pdfrw:
from pdfrw import PdfDict, PdfName, PdfArray, PdfObject
- else:
+ from pdfrw.py23_diffs import convert_load
+ elif self.engine == Engine.internal:
PdfDict = MyPdfDict
PdfName = MyPdfName
PdfObject = MyPdfObject
PdfArray = MyPdfArray
- NullObject = PdfObject("null")
- TrueObject = PdfObject("true")
+ convert_load = my_convert_load
+ else:
+ raise ValueError("unknown engine: %s" % self.engine)
+ NullObject = None if self.engine == Engine.pikepdf else PdfObject("null")
+ TrueObject = True if self.engine == Engine.pikepdf else PdfObject("true")
# We fill the catalog with more information like /ViewerPreferences,
# /PageMode, /PageLayout or /OpenAction because the latter refers to a
@@ -841,10 +1102,14 @@ class pdfdoc(object):
# is added, so we can only start using it after all pages have been
# written.
- if self.with_pdfrw:
+ if self.engine == Engine.pikepdf:
+ catalog = self.writer.Root
+ elif self.engine == Engine.pdfrw:
catalog = self.writer.trailer.Root
- else:
+ elif self.engine == Engine.internal:
catalog = self.writer.catalog
+ else:
+ raise ValueError("unknown engine: %s" % self.engine)
if (
self.fullscreen
@@ -900,17 +1165,35 @@ class pdfdoc(object):
# FitBV - Fits the height of the page bounding box to the window.
# by default the initial page is the first one
- initial_page = self.writer.pagearray[0]
+ if self.engine == Engine.pikepdf:
+ initial_page = self.writer.pages[0]
+ else:
+ initial_page = self.writer.pagearray[0]
# we set the open action here to make sure we open on the requested
# initial page but this value might be overwritten by a custom open
# action later while still taking the requested initial page into
# account
if self.initial_page is not None:
- initial_page = self.writer.pagearray[self.initial_page - 1]
+ if self.engine == Engine.pikepdf:
+ initial_page = self.writer.pages[self.initial_page - 1]
+ else:
+ initial_page = self.writer.pagearray[self.initial_page - 1]
catalog[PdfName.OpenAction] = PdfArray(
[initial_page, PdfName.XYZ, NullObject, NullObject, 0]
)
+ # The /OpenAction array must contain the page as an indirect object.
+ # This changed some time after 4.2.0 and on or before 5.0.0 and current
+ # versions require to use .obj or otherwise we get:
+ # TypeError: Can't convert ObjectHelper (or subclass) to Object
+ # implicitly. Use .obj to get access the underlying object.
+ # See https://github.com/pikepdf/pikepdf/issues/313 for details.
+ if self.engine == Engine.pikepdf:
+ if isinstance(initial_page, pikepdf.Page):
+ initial_page = self.writer.make_indirect(initial_page.obj)
+ else:
+ initial_page = self.writer.make_indirect(initial_page)
+
if self.magnification == Magnification.fit:
catalog[PdfName.OpenAction] = PdfArray([initial_page, PdfName.Fit])
elif self.magnification == Magnification.fith:
@@ -941,24 +1224,84 @@ class pdfdoc(object):
catalog[PdfName.PageLayout] = PdfName.TwoColumnRight
elif self.page_layout == PageLayout.twocolumnleft:
catalog[PdfName.PageLayout] = PdfName.TwoColumnLeft
+ elif self.page_layout == PageLayout.twopageright:
+ catalog[PdfName.PageLayout] = PdfName.TwoPageRight
+ if self.output_version < "1.5":
+ self.output_version = "1.5"
+ elif self.page_layout == PageLayout.twopageleft:
+ catalog[PdfName.PageLayout] = PdfName.TwoPageLeft
+ if self.output_version < "1.5":
+ self.output_version = "1.5"
elif self.page_layout is None:
pass
else:
raise ValueError("unknown page layout: %s" % self.page_layout)
+ if self.pdfa is not None:
+ if self.engine == Engine.pikepdf:
+ metadata = self.writer.make_stream(self.xmp)
+ else:
+ metadata = PdfDict(stream=convert_load(self.xmp))
+ metadata[PdfName.Subtype] = PdfName.XML
+ metadata[PdfName.Type] = PdfName.Metadata
+ with open(self.pdfa, "rb") as f:
+ icc = f.read()
+ intents = PdfDict()
+ if self.engine == Engine.pikepdf:
+ iccstream = self.writer.make_stream(icc)
+ iccstream.stream_dict.N = 3
+ else:
+ iccstream = PdfDict(stream=convert_load(zlib.compress(icc)))
+ iccstream[PdfName.N] = 3
+ iccstream[PdfName.Filter] = PdfName.FlateDecode
+ intents[PdfName.S] = PdfName.GTS_PDFA1
+ intents[PdfName.Type] = PdfName.OutputIntent
+ intents[PdfName.OutputConditionIdentifier] = (
+ b"sRGB" if self.engine == Engine.pikepdf else b"(sRGB)"
+ )
+ intents[PdfName.DestOutputProfile] = iccstream
+ catalog[PdfName.OutputIntents] = PdfArray([intents])
+ catalog[PdfName.Metadata] = metadata
+
+ if self.engine == Engine.internal:
+ self.writer.addobj(metadata)
+ self.writer.addobj(iccstream)
+
# now write out the PDF
- if self.with_pdfrw:
- self.writer.trailer.Info = self.info
+ if self.engine == Engine.pikepdf:
+ kwargs = {}
+ if pikepdf.__version__ >= "6.2.0":
+ kwargs["deterministic_id"] = True
+ self.writer.save(
+ outputstream, min_version=self.output_version, linearize=True, **kwargs
+ )
+ elif self.engine == Engine.pdfrw:
+ self.writer.trailer.Info = self.writer.docinfo
+ # setting the version attribute of the pdfrw PdfWriter object will
+ # influence the behaviour of the write() function
+ self.writer.version = self.output_version
+ if self.pdfa:
+ md5 = hashlib.md5(b"").hexdigest().encode("ascii")
+ self.writer.trailer[PdfName.ID] = PdfArray([md5, md5])
self.writer.write(outputstream)
+ elif self.engine == Engine.internal:
+ self.writer.tostream(
+ self.writer.docinfo,
+ outputstream,
+ self.output_version,
+ None if self.pdfa is None else b"",
+ )
else:
- self.writer.tostream(self.info, outputstream)
+ raise ValueError("unknown engine: %s" % self.engine)
-def get_imgmetadata(imgdata, imgformat, default_dpi, colorspace, rawdata=None):
+def get_imgmetadata(
+ imgdata, imgformat, default_dpi, colorspace, rawdata=None, rotreq=None
+):
if imgformat == ImageFormat.JPEG2000 and rawdata is not None and imgdata is None:
# this codepath gets called if the PIL installation is not able to
# handle JPEG2000 files
- imgwidthpx, imgheightpx, ics, hdpi, vdpi = parsejp2(rawdata)
+ imgwidthpx, imgheightpx, ics, hdpi, vdpi, channels, bpp = jp2.parse(rawdata)
if hdpi is None:
hdpi = default_dpi
@@ -968,7 +1311,19 @@ def get_imgmetadata(imgdata, imgformat, default_dpi, colorspace, rawdata=None):
else:
imgwidthpx, imgheightpx = imgdata.size
- ndpi = imgdata.info.get("dpi", (default_dpi, default_dpi))
+ ndpi = imgdata.info.get("dpi")
+ if ndpi is None:
+ # the PNG plugin of PIL adds the undocumented "aspect" field instead of
+ # the "dpi" field if the PNG pHYs chunk unit is not set to meters
+ if imgformat == ImageFormat.PNG and imgdata.info.get("aspect") is not None:
+ aspect = imgdata.info["aspect"]
+ # make sure not to go below the default dpi
+ if aspect[0] > aspect[1]:
+ ndpi = (default_dpi * aspect[0] / aspect[1], default_dpi)
+ else:
+ ndpi = (default_dpi, default_dpi * aspect[1] / aspect[0])
+ else:
+ ndpi = (default_dpi, default_dpi)
# In python3, the returned dpi value for some tiff images will
# not be an integer but a float. To make the behaviour of
# img2pdf the same between python2 and python3, we convert that
@@ -977,17 +1332,26 @@ def get_imgmetadata(imgdata, imgformat, default_dpi, colorspace, rawdata=None):
ndpi = (int(round(ndpi[0])), int(round(ndpi[1])))
ics = imgdata.mode
- if ics in ["LA", "PA", "RGBA"] or "transparency" in imgdata.info:
- logging.warning(
- "Image contains transparency which cannot be retained " "in PDF."
- )
- logging.warning("img2pdf will not perform a lossy operation.")
- logging.warning("You can remove the alpha channel using imagemagick:")
- logging.warning(
- " $ convert input.png -background white -alpha "
- "remove -alpha off output.png"
- )
- raise Exception("Refusing to work on images with alpha channel")
+ # GIF and PNG files with transparency are supported
+ if imgformat in [ImageFormat.PNG, ImageFormat.GIF, ImageFormat.JPEG2000] and (
+ ics in ["RGBA", "LA"] or "transparency" in imgdata.info
+ ):
+ # Must check the IHDR chunk for the bit depth, because PIL would lossily
+ # convert 16-bit RGBA/LA images to 8-bit.
+ if imgformat == ImageFormat.PNG and rawdata is not None:
+ depth = rawdata[24]
+ if depth > 8:
+ logger.warning("Image with transparency and a bit depth of %d." % depth)
+ logger.warning("This is unsupported due to PIL limitations.")
+ logger.warning(
+ "If you accept a lossy conversion, you can manually convert "
+ "your images to 8 bit using `convert -depth 8` from imagemagick"
+ )
+ raise AlphaChannelError(
+ "Refusing to work with multiple >8bit channels."
+ )
+ elif ics in ["LA", "PA", "RGBA"] or "transparency" in imgdata.info:
+ raise AlphaChannelError("This function must not be called on images with alpha")
# Since commit 07a96209597c5e8dfe785c757d7051ce67a980fb or release 4.1.0
# Pillow retrieves the DPI from EXIF if it cannot find the DPI in the JPEG
@@ -1004,37 +1368,61 @@ def get_imgmetadata(imgdata, imgformat, default_dpi, colorspace, rawdata=None):
imgdata.tag_v2.get(TiffImagePlugin.Y_RESOLUTION, default_dpi),
)
- logging.debug("input dpi = %d x %d", *ndpi)
+ logger.debug("input dpi = %d x %d", *ndpi)
rotation = 0
- if hasattr(imgdata, "_getexif") and imgdata._getexif() is not None:
- for tag, value in imgdata._getexif().items():
- if TAGS.get(tag, tag) == "Orientation":
- # Detailed information on EXIF rotation tags:
- # http://impulseadventure.com/photo/exif-orientation.html
- if value == 1:
- rotation = 0
- elif value == 6:
- rotation = 90
- elif value == 3:
- rotation = 180
- elif value == 8:
- rotation = 270
- elif value in (2, 4, 5, 7):
- raise Exception(
- 'Image "%s": Unsupported flipped '
- "rotation mode (%d)" % (im.name, value)
- )
- else:
- raise Exception(
- 'Image "%s": invalid rotation (%d)' % (im.name, value)
- )
+ if rotreq in (None, Rotation.auto, Rotation.ifvalid):
+ if hasattr(imgdata, "_getexif") and imgdata._getexif() is not None:
+ for tag, value in imgdata._getexif().items():
+ if TAGS.get(tag, tag) == "Orientation":
+ # Detailed information on EXIF rotation tags:
+ # http://impulseadventure.com/photo/exif-orientation.html
+ if value == 1:
+ rotation = 0
+ elif value == 6:
+ rotation = 90
+ elif value == 3:
+ rotation = 180
+ elif value == 8:
+ rotation = 270
+ elif value in (2, 4, 5, 7):
+ if rotreq == Rotation.ifvalid:
+ logger.warning(
+ "Unsupported flipped rotation mode (%d): use "
+ "--rotation=ifvalid or "
+ "rotation=img2pdf.Rotation.ifvalid to ignore",
+ value,
+ )
+ else:
+ raise ExifOrientationError(
+ "Unsupported flipped rotation mode (%d): use "
+ "--rotation=ifvalid or "
+ "rotation=img2pdf.Rotation.ifvalid to ignore" % value
+ )
+ else:
+ if rotreq == Rotation.ifvalid:
+ logger.warning("Invalid rotation (%d)", value)
+ else:
+ raise ExifOrientationError(
+ "Invalid rotation (%d): use --rotation=ifvalid "
+ "or rotation=img2pdf.Rotation.ifvalid to ignore" % value
+ )
+ elif rotreq in (Rotation.none, Rotation["0"]):
+ rotation = 0
+ elif rotreq == Rotation["90"]:
+ rotation = 90
+ elif rotreq == Rotation["180"]:
+ rotation = 180
+ elif rotreq == Rotation["270"]:
+ rotation = 270
+ else:
+ raise Exception("invalid rotreq")
- logging.debug("rotation = %d°", rotation)
+ logger.debug("rotation = %d°", rotation)
if colorspace:
color = colorspace
- logging.debug("input colorspace (forced) = %s", color)
+ logger.debug("input colorspace (forced) = %s", color)
else:
color = None
for c in Colorspace:
@@ -1064,11 +1452,62 @@ def get_imgmetadata(imgdata, imgformat, default_dpi, colorspace, rawdata=None):
# with the first approach for now.
if "adobe" in imgdata.info:
color = Colorspace["CMYK;I"]
- logging.debug("input colorspace = %s", color.name)
+ logger.debug("input colorspace = %s", color.name)
+
+ iccp = None
+ if "icc_profile" in imgdata.info:
+ iccp = imgdata.info.get("icc_profile")
+ # GIMP saves bilevel TIFF images and palette PNG images with only black and
+ # white in the palette with an RGB ICC profile which is useless
+ # https://gitlab.gnome.org/GNOME/gimp/-/issues/3438
+ # and produces an error in Adobe Acrobat, so we ignore it with a warning.
+ # imagemagick also used to (wrongly) include an RGB ICC profile for bilevel
+ # images: https://github.com/ImageMagick/ImageMagick/issues/2070
+ if iccp is not None and (
+ (color == Colorspace["1"] and imgformat == ImageFormat.TIFF)
+ or (
+ imgformat == ImageFormat.PNG
+ and color == Colorspace.P
+ and rawdata is not None
+ and parse_png(rawdata)[1]
+ in [b"\x00\x00\x00\xff\xff\xff", b"\xff\xff\xff\x00\x00\x00"]
+ )
+ ):
+ with io.BytesIO(iccp) as f:
+ prf = ImageCms.ImageCmsProfile(f)
+ if (
+ prf.profile.model == "sRGB"
+ and prf.profile.manufacturer == "GIMP"
+ and prf.profile.profile_description == "GIMP built-in sRGB"
+ ):
+ if imgformat == ImageFormat.TIFF:
+ logger.warning(
+ "Ignoring RGB ICC profile in bilevel TIFF produced by GIMP."
+ )
+ elif imgformat == ImageFormat.PNG:
+ logger.warning(
+ "Ignoring RGB ICC profile in 2-color palette PNG produced by GIMP."
+ )
+ logger.warning("https://gitlab.gnome.org/GNOME/gimp/-/issues/3438")
+ iccp = None
+ # SmartAlbums old version (found 2.2.6) exports JPG with only 1 compone
+ # with an RGB ICC profile which is useless.
+ # This produces an error in Adobe Acrobat, so we ignore it with a warning.
+ # Update: Found another case, the JPG is created by Adobe PhotoShop, so we
+ # don't check software anymore.
+ if iccp is not None and (
+ (color == Colorspace["L"] and imgformat == ImageFormat.JPEG)
+ ):
+ with io.BytesIO(iccp) as f:
+ prf = ImageCms.ImageCmsProfile(f)
+
+ if prf.profile.xcolor_space not in ("GRAY"):
+ logger.warning("Ignoring non-GRAY ICC profile in Grayscale JPG")
+ iccp = None
- logging.debug("width x height = %dpx x %dpx", imgwidthpx, imgheightpx)
+ logger.debug("width x height = %dpx x %dpx", imgwidthpx, imgheightpx)
- return (color, ndpi, imgwidthpx, imgheightpx, rotation)
+ return (color, ndpi, imgwidthpx, imgheightpx, rotation, iccp)
def ccitt_payload_location_from_pil(img):
@@ -1083,19 +1522,20 @@ def ccitt_payload_location_from_pil(img):
# Read the TIFF tags to find the offset(s) of the compressed data strips.
strip_offsets = img.tag_v2[TiffImagePlugin.STRIPOFFSETS]
strip_bytes = img.tag_v2[TiffImagePlugin.STRIPBYTECOUNTS]
- rows_per_strip = img.tag_v2.get(TiffImagePlugin.ROWSPERSTRIP, 2 ** 32 - 1)
# PIL always seems to create a single strip even for very large TIFFs when
# it saves images, so assume we only have to read a single strip.
# A test ~10 GPixel image was still encoded as a single strip. Just to be
# safe check throw an error if there is more than one offset.
if len(strip_offsets) != 1 or len(strip_bytes) != 1:
- raise NotImplementedError("Transcoding multiple strips not supported")
+ raise NotImplementedError(
+ "Transcoding multiple strips not supported by the PDF format"
+ )
(offset,), (length,) = strip_offsets, strip_bytes
- logging.debug("TIFF strip_offsets: %d" % offset)
- logging.debug("TIFF strip_bytes: %d" % length)
+ logger.debug("TIFF strip_offsets: %d" % offset)
+ logger.debug("TIFF strip_bytes: %d" % length)
return offset, length
@@ -1103,7 +1543,7 @@ def ccitt_payload_location_from_pil(img):
def transcode_monochrome(imgdata):
"""Convert the open PIL.Image imgdata to compressed CCITT Group4 data"""
- logging.debug("Converting monochrome to CCITT Group4")
+ logger.debug("Converting monochrome to CCITT Group4")
# Convert the image to Group 4 in memory. If libtiff is not installed and
# Pillow is not compiled against it, .save() will raise an exception.
@@ -1114,7 +1554,35 @@ def transcode_monochrome(imgdata):
# killed by a SIGABRT:
# https://gitlab.mister-muffin.de/josch/img2pdf/issues/46
im = Image.frombytes(imgdata.mode, imgdata.size, imgdata.tobytes())
- im.save(newimgio, format="TIFF", compression="group4")
+
+ # Since version 8.3.0 Pillow limits strips to 64 KB. Since PDF only
+ # supports single strip CCITT Group4 payloads, we have to coerce it back
+ # into putting everything into a single strip. Thanks to Andrew Murray for
+ # the hack.
+ #
+ # Since version 8.4.0 Pillow allows us to modify the strip size explicitly
+ tmp_strip_size = (imgdata.size[0] + 7) // 8 * imgdata.size[1]
+ if hasattr(TiffImagePlugin, "STRIP_SIZE"):
+ # we are using Pillow 8.4.0 or later
+ with temp_attr(TiffImagePlugin, "STRIP_SIZE", tmp_strip_size):
+ im.save(newimgio, format="TIFF", compression="group4")
+ else:
+ # only needed for Pillow 8.3.x but works for versions before that as
+ # well
+ pillow__getitem__ = TiffImagePlugin.ImageFileDirectory_v2.__getitem__
+
+ def __getitem__(self, tag):
+ overrides = {
+ TiffImagePlugin.ROWSPERSTRIP: imgdata.size[1],
+ TiffImagePlugin.STRIPBYTECOUNTS: [tmp_strip_size],
+ TiffImagePlugin.STRIPOFFSETS: [0],
+ }
+ return overrides.get(tag, pillow__getitem__(self, tag))
+
+ with temp_attr(
+ TiffImagePlugin.ImageFileDirectory_v2, "__getitem__", __getitem__
+ ):
+ im.save(newimgio, format="TIFF", compression="group4")
# Open new image in memory
newimgio.seek(0)
@@ -1128,36 +1596,220 @@ def transcode_monochrome(imgdata):
def parse_png(rawdata):
pngidat = b""
- palette = []
+ palette = b""
i = 16
while i < len(rawdata):
# once we can require Python >= 3.2 we can use int.from_bytes() instead
- n, = struct.unpack(">I", rawdata[i - 8 : i - 4])
+ (n,) = struct.unpack(">I", rawdata[i - 8 : i - 4])
if i + n > len(rawdata):
raise Exception("invalid png: %d %d %d" % (i, n, len(rawdata)))
if rawdata[i - 4 : i] == b"IDAT":
pngidat += rawdata[i : i + n]
elif rawdata[i - 4 : i] == b"PLTE":
- # This could be as simple as saying "palette = rawdata[i:i+n]" but
- # pdfrw does only escape parenthesis and backslashes in the raw
- # byte stream. But raw carriage return bytes are interpreted as
- # line feed bytes by ghostscript. So instead we use the hex string
- # format. pdfrw cannot write it but at least ghostscript is happy
- # with it. We would also write out the palette in binary format
- # (and escape more bytes) but since we cannot use pdfrw anyways,
- # we choose the more human readable variant.
- # See https://github.com/pmaupin/pdfrw/issues/147
- for j in range(i, i + n, 3):
- # with int.from_bytes() we would not have to prepend extra
- # zeroes
- color, = struct.unpack(">I", b"\x00" + rawdata[j : j + 3])
- palette.append(color)
+ palette += rawdata[i : i + n]
i += n
i += 12
return pngidat, palette
-def read_images(rawdata, colorspace, first_frame_only=False):
+miff_re = re.compile(
+ r"""
+ [^\x00-\x20\x7f-\x9f] # the field name must not start with a control char or space
+ [^=]+ # the field name can even contain spaces
+ = # field name and value are separated by an equal sign
+ (?:
+ [^\x00-\x20\x7f-\x9f{}] # either chars that are not braces and not control chars
+ |{[^}]*} # or any kind of char surrounded by braces
+ )+""",
+ re.VERBOSE,
+)
+
+# https://imagemagick.org/script/miff.php
+# turn off black formatting until python 3.10 is available on more platforms
+# and we can use match/case
+# fmt: off
+def parse_miff(data):
+ results = []
+ header, rest = data.split(b":\x1a", 1)
+ header = header.decode("ISO-8859-1")
+ assert header.lower().startswith("id=imagemagick")
+ hdata = {}
+ for i, line in enumerate(re.findall(miff_re, header)):
+ if not line:
+ continue
+ k, v = line.split("=", 1)
+ if i == 0:
+ assert k.lower() == "id"
+ assert v.lower() == "imagemagick"
+ #match k.lower():
+ # case "class":
+ if k.lower() == "class":
+ #match v:
+ # case "DirectClass" | "PseudoClass":
+ if v in ["DirectClass", "PseudoClass"]:
+ hdata["class"] = v
+ # case _:
+ else:
+ print("cannot understand class", v)
+ # case "colorspace":
+ elif k.lower() == "colorspace":
+ # theoretically RGBA and CMYKA should be supported as well
+ # please teach me how to create such a MIFF file
+ #match v:
+ # case "sRGB" | "CMYK" | "Gray":
+ if v in ["sRGB", "CMYK", "Gray"]:
+ hdata["colorspace"] = v
+ # case _:
+ else:
+ print("cannot understand colorspace", v)
+ # case "depth":
+ elif k.lower() == "depth":
+ #match v:
+ # case "8" | "16" | "32":
+ if v in ["8", "16", "32"]:
+ hdata["depth"] = int(v)
+ # case _:
+ else:
+ print("cannot understand depth", v)
+ # case "colors":
+ elif k.lower() == "colors":
+ hdata["colors"] = int(v)
+ # case "matte":
+ elif k.lower() == "matte":
+ #match v:
+ # case "True":
+ if v == "True":
+ hdata["matte"] = True
+ # case "False":
+ elif v == "False":
+ hdata["matte"] = False
+ # case _:
+ else:
+ print("cannot understand matte", v)
+ # case "columns" | "rows":
+ elif k.lower() in ["columns", "rows"]:
+ hdata[k.lower()] = int(v)
+ # case "compression":
+ elif k.lower() == "compression":
+ print("compression not yet supported")
+ # case "profile":
+ elif k.lower() == "profile":
+ assert v in ["icc", "exif"]
+ hdata["profile"] = v
+ # case "resolution":
+ elif k.lower() == "resolution":
+ dpix, dpiy = v.split("x", 1)
+ hdata["resolution"] = (float(dpix), float(dpiy))
+
+ assert "depth" in hdata
+ assert "columns" in hdata
+ assert "rows" in hdata
+ #match hdata["class"]:
+ # case "DirectClass":
+ if hdata["class"] == "DirectClass":
+ if "colors" in hdata:
+ assert hdata["colors"] == 0
+ #match hdata["colorspace"]:
+ # case "sRGB":
+ if hdata["colorspace"] == "sRGB":
+ numchannels = 3
+ colorspace = Colorspace.RGB
+ # case "CMYK":
+ elif hdata["colorspace"] == "CMYK":
+ numchannels = 4
+ colorspace = Colorspace.CMYK
+ # case "Gray":
+ elif hdata["colorspace"] == "Gray":
+ numchannels = 1
+ colorspace = Colorspace.L
+ if hdata.get("matte"):
+ numchannels += 1
+ if hdata.get("profile"):
+ # there is no key encoding the length of icc or exif data
+ # according to the docs, the profile-icc key is supposed to do this
+ print("FAIL: exif")
+ else:
+ lenimgdata = (
+ hdata["depth"] // 8 * numchannels * hdata["columns"] * hdata["rows"]
+ )
+ assert len(rest) >= lenimgdata, (
+ len(rest),
+ hdata["depth"],
+ numchannels,
+ hdata["columns"],
+ hdata["rows"],
+ lenimgdata,
+ )
+ if colorspace == Colorspace.RGB and hdata["depth"] == 8:
+ newimg = Image.frombytes("RGB", (hdata["columns"], hdata["rows"]), rest[:lenimgdata])
+ imgdata, palette, depth = to_png_data(newimg)
+ assert palette == b""
+ assert depth == hdata["depth"]
+ imgfmt = ImageFormat.PNG
+ else:
+ imgdata = zlib.compress(rest[:lenimgdata])
+ imgfmt = ImageFormat.MIFF
+ results.append(
+ (
+ colorspace,
+ hdata.get("resolution") or (default_dpi, default_dpi),
+ imgfmt,
+ imgdata,
+ None, # smask
+ hdata["columns"],
+ hdata["rows"],
+ [], # palette
+ False, # inverted
+ hdata["depth"],
+ 0, # rotation
+ None, # icc profile
+ )
+ )
+ if len(rest) > lenimgdata:
+ # another image is here
+ assert rest[lenimgdata:][:14].lower() == b"id=imagemagick"
+ results.extend(parse_miff(rest[lenimgdata:]))
+ # case "PseudoClass":
+ elif hdata["class"] == "PseudoClass":
+ assert "colors" in hdata
+ if hdata.get("matte"):
+ numchannels = 2
+ else:
+ numchannels = 1
+ lenpal = 3 * hdata["colors"] * hdata["depth"] // 8
+ lenimgdata = numchannels * hdata["rows"] * hdata["columns"]
+ assert len(rest) >= lenpal + lenimgdata, (len(rest), lenpal, lenimgdata)
+ results.append(
+ (
+ Colorspace.RGB,
+ hdata.get("resolution") or (default_dpi, default_dpi),
+ ImageFormat.MIFF,
+ zlib.compress(rest[lenpal : lenpal + lenimgdata]),
+ None, # FIXME: allow alpha channel smask
+ hdata["columns"],
+ hdata["rows"],
+ rest[:lenpal], # palette
+ False, # inverted
+ hdata["depth"],
+ 0, # rotation
+ None, # icc profile
+ )
+ )
+ if len(rest) > lenpal + lenimgdata:
+ # another image is here
+ assert rest[lenpal + lenimgdata :][:14].lower() == b"id=imagemagick", (
+ len(rest),
+ lenpal,
+ lenimgdata,
+ )
+ results.extend(parse_miff(rest[lenpal + lenimgdata :]))
+ return results
+# fmt: on
+
+
+def read_images(
+ rawdata, colorspace, first_frame_only=False, rot=None, include_thumbnails=False
+):
im = BytesIO(rawdata)
im.seek(0)
imgdata = None
@@ -1165,14 +1817,21 @@ def read_images(rawdata, colorspace, first_frame_only=False):
imgdata = Image.open(im)
except IOError as e:
# test if it is a jpeg2000 image
- if rawdata[:12] != b"\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A":
+ if rawdata[:12] == b"\x00\x00\x00\x0C\x6A\x50\x20\x20\x0D\x0A\x87\x0A":
+ # image is jpeg2000
+ imgformat = ImageFormat.JPEG2000
+ if rawdata[:14].lower() == b"id=imagemagick":
+ # image is in MIFF format
+ # this is useful for 16 bit CMYK because PNG cannot do CMYK and thus
+ # we need PIL but PIL cannot do 16 bit
+ imgformat = ImageFormat.MIFF
+ else:
raise ImageOpenError(
"cannot read input image (not jpeg2000). "
"PIL: error reading image: %s" % e
)
- # image is jpeg2000
- imgformat = ImageFormat.JPEG2000
else:
+ logger.debug("PIL format = %s", imgdata.format)
imgformat = None
for f in ImageFormat:
if f.name == imgdata.format:
@@ -1180,39 +1839,185 @@ def read_images(rawdata, colorspace, first_frame_only=False):
if imgformat is None:
imgformat = ImageFormat.other
- logging.debug("imgformat = %s", imgformat.name)
+ def cleanup():
+ if imgdata is not None:
+ # the python-pil version 2.3.0-1ubuntu3 in Ubuntu does not have the
+ # close() method
+ try:
+ imgdata.close()
+ except AttributeError:
+ pass
+ im.close()
+
+ logger.debug("imgformat = %s", imgformat.name)
# depending on the input format, determine whether to pass the raw
# image or the zlib compressed color information
# JPEG and JPEG2000 can be embedded into the PDF as-is
if imgformat == ImageFormat.JPEG or imgformat == ImageFormat.JPEG2000:
- color, ndpi, imgwidthpx, imgheightpx, rotation = get_imgmetadata(
- imgdata, imgformat, default_dpi, colorspace, rawdata
+ color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
+ imgdata, imgformat, default_dpi, colorspace, rawdata, rot
)
if color == Colorspace["1"]:
raise JpegColorspaceError("jpeg can't be monochrome")
if color == Colorspace["P"]:
raise JpegColorspaceError("jpeg can't have a color palette")
- if color == Colorspace["RGBA"]:
+ if color == Colorspace["RGBA"] and imgformat != ImageFormat.JPEG2000:
raise JpegColorspaceError("jpeg can't have an alpha channel")
- im.close()
- logging.debug("read_images() embeds a JPEG")
+ logger.debug("read_images() embeds a JPEG")
+ cleanup()
+ depth = 8
+ if imgformat == ImageFormat.JPEG2000:
+ *_, depth = jp2.parse(rawdata)
return [
(
color,
ndpi,
imgformat,
rawdata,
+ None,
imgwidthpx,
imgheightpx,
[],
False,
- 8,
+ depth,
rotation,
+ iccp,
)
]
+ # The MPO format is multiple JPEG images concatenated together
+ # we use the offset and size information to dissect the MPO into its
+ # individual JPEG images and then embed those into the PDF individually.
+ #
+ # The downside is, that this truncates the first JPEG as the MPO metadata
+ # will still be in it but the referenced images are chopped off. We still
+ # do it that way instead of adding the full MPO as the first image to not
+ # store duplicate image data.
+ if imgformat == ImageFormat.MPO:
+ result = []
+ img_page_count = 0
+ assert len(imgdata._MpoImageFile__mpoffsets) == len(imgdata.mpinfo[0xB002])
+ num_frames = len(imgdata.mpinfo[0xB002])
+ # An MPO file can be a main image together with one or more thumbnails
+ # if that is the case, then we only include all frames if the
+ # --include-thumbnails option is given. If it is not, such an MPO file
+ # will be embedded as is, so including its thumbnails but showing up
+ # as a single image page in the resulting PDF.
+ num_main_frames = 0
+ num_thumbnail_frames = 0
+ for i, mpent in enumerate(imgdata.mpinfo[0xB002]):
+ # check only the first frame for being the main image
+ if (
+ i == 0
+ and mpent["Attribute"]["DependentParentImageFlag"]
+ and not mpent["Attribute"]["DependentChildImageFlag"]
+ and mpent["Attribute"]["RepresentativeImageFlag"]
+ and mpent["Attribute"]["MPType"] == "Baseline MP Primary Image"
+ ):
+ num_main_frames += 1
+ elif (
+ not mpent["Attribute"]["DependentParentImageFlag"]
+ and mpent["Attribute"]["DependentChildImageFlag"]
+ and not mpent["Attribute"]["RepresentativeImageFlag"]
+ and mpent["Attribute"]["MPType"]
+ in [
+ "Large Thumbnail (VGA Equivalent)",
+ "Large Thumbnail (Full HD Equivalent)",
+ ]
+ ):
+ num_thumbnail_frames += 1
+ logger.debug(f"number of frames: {num_frames}")
+ logger.debug(f"number of main frames: {num_main_frames}")
+ logger.debug(f"number of thumbnail frames: {num_thumbnail_frames}")
+ # this MPO file is a main image plus zero or more thumbnails
+ # embed as-is unless the --include-thumbnails option was given
+ if num_frames == 1 or (
+ not include_thumbnails
+ and num_main_frames == 1
+ and num_thumbnail_frames + 1 == num_frames
+ ):
+ color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
+ imgdata, imgformat, default_dpi, colorspace, rawdata, rot
+ )
+ if color == Colorspace["1"]:
+ raise JpegColorspaceError("jpeg can't be monochrome")
+ if color == Colorspace["P"]:
+ raise JpegColorspaceError("jpeg can't have a color palette")
+ if color == Colorspace["RGBA"]:
+ raise JpegColorspaceError("jpeg can't have an alpha channel")
+ logger.debug("read_images() embeds an MPO verbatim")
+ cleanup()
+ return [
+ (
+ color,
+ ndpi,
+ ImageFormat.JPEG,
+ rawdata,
+ None,
+ imgwidthpx,
+ imgheightpx,
+ [],
+ False,
+ 8,
+ rotation,
+ iccp,
+ )
+ ]
+ # If the control flow reaches here, the MPO has more than a single
+ # frame but was not detected to be a main image followed by multiple
+ # thumbnails. We thus treat this MPO as we do other multi-frame images
+ # and include all its frames as individual pages.
+ for offset, mpent in zip(
+ imgdata._MpoImageFile__mpoffsets, imgdata.mpinfo[0xB002]
+ ):
+ if first_frame_only and img_page_count > 0:
+ break
+ with BytesIO(rawdata[offset : offset + mpent["Size"]]) as rawframe:
+ with Image.open(rawframe) as imframe:
+ # The first frame contains the data that makes the JPEG a MPO
+ # Could we thus embed an MPO into another MPO? Lets not support
+ # such madness ;)
+ if img_page_count > 0 and imframe.format != "JPEG":
+ raise Exception("MPO payload must be a JPEG %s", imframe.format)
+ (
+ color,
+ ndpi,
+ imgwidthpx,
+ imgheightpx,
+ rotation,
+ iccp,
+ ) = get_imgmetadata(
+ imframe, ImageFormat.JPEG, default_dpi, colorspace, rotreq=rot
+ )
+ if color == Colorspace["1"]:
+ raise JpegColorspaceError("jpeg can't be monochrome")
+ if color == Colorspace["P"]:
+ raise JpegColorspaceError("jpeg can't have a color palette")
+ if color == Colorspace["RGBA"]:
+ raise JpegColorspaceError("jpeg can't have an alpha channel")
+ logger.debug("read_images() embeds a JPEG from MPO")
+ result.append(
+ (
+ color,
+ ndpi,
+ ImageFormat.JPEG,
+ rawdata[offset : offset + mpent["Size"]],
+ None,
+ imgwidthpx,
+ imgheightpx,
+ [],
+ False,
+ 8,
+ rotation,
+ iccp,
+ )
+ )
+ img_page_count += 1
+ cleanup()
+ return result
+
# We can directly embed the IDAT chunk of PNG images if the PNG is not
# interlaced
#
@@ -1221,33 +2026,48 @@ def read_images(rawdata, colorspace, first_frame_only=False):
# IHDR chunk. We know where to find that in the file because the IHDR chunk
# must be the first chunk.
if imgformat == ImageFormat.PNG and rawdata[28] == 0:
- color, ndpi, imgwidthpx, imgheightpx, rotation = get_imgmetadata(
- imgdata, imgformat, default_dpi, colorspace, rawdata
+ color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
+ imgdata, imgformat, default_dpi, colorspace, rawdata, rot
)
- pngidat, palette = parse_png(rawdata)
- im.close()
- # PIL does not provide the information about the original bits per
- # sample. Thus, we retrieve that info manually by looking at byte 9 in
- # the IHDR chunk. We know where to find that in the file because the
- # IHDR chunk must be the first chunk
- depth = rawdata[24]
- if depth not in [1, 2, 4, 8, 16]:
- raise ValueError("invalid bit depth: %d" % depth)
- logging.debug("read_images() embeds a PNG")
- return [
- (
- color,
- ndpi,
- imgformat,
- pngidat,
- imgwidthpx,
- imgheightpx,
- palette,
- False,
- depth,
- rotation,
- )
- ]
+ if (
+ color != Colorspace.RGBA
+ and color != Colorspace.LA
+ and color != Colorspace.PA
+ and "transparency" not in imgdata.info
+ ):
+ pngidat, palette = parse_png(rawdata)
+ # PIL does not provide the information about the original bits per
+ # sample. Thus, we retrieve that info manually by looking at byte 9 in
+ # the IHDR chunk. We know where to find that in the file because the
+ # IHDR chunk must be the first chunk
+ depth = rawdata[24]
+ if depth not in [1, 2, 4, 8, 16]:
+ raise ValueError("invalid bit depth: %d" % depth)
+ # we embed the PNG only if it is not at the same time palette based
+ # and has an icc profile because PDF doesn't support icc profiles
+ # on palette images
+ if palette == b"" or iccp is None:
+ logger.debug("read_images() embeds a PNG")
+ cleanup()
+ return [
+ (
+ color,
+ ndpi,
+ imgformat,
+ pngidat,
+ None,
+ imgwidthpx,
+ imgheightpx,
+ palette,
+ False,
+ depth,
+ rotation,
+ iccp,
+ )
+ ]
+
+ if imgformat == ImageFormat.MIFF:
+ return parse_miff(rawdata)
# If our input is not JPEG or PNG, then we might have a format that
# supports multiple frames (like TIFF or GIF), so we need a loop to
@@ -1292,6 +2112,7 @@ def read_images(rawdata, colorspace, first_frame_only=False):
imgformat == ImageFormat.TIFF
and imgdata.info["compression"] == "group4"
and len(imgdata.tag_v2[TiffImagePlugin.STRIPOFFSETS]) == 1
+ and len(imgdata.tag_v2[TiffImagePlugin.STRIPBYTECOUNTS]) == 1
):
photo = imgdata.tag_v2[TiffImagePlugin.PHOTOMETRIC_INTERPRETATION]
inverted = False
@@ -1302,8 +2123,8 @@ def read_images(rawdata, colorspace, first_frame_only=False):
"unsupported photometric interpretation for "
"group4 tiff: %d" % photo
)
- color, ndpi, imgwidthpx, imgheightpx, rotation = get_imgmetadata(
- imgdata, imgformat, default_dpi, colorspace, rawdata
+ color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
+ imgdata, imgformat, default_dpi, colorspace, rawdata, rot
)
offset, length = ccitt_payload_location_from_pil(imgdata)
im.seek(offset)
@@ -1316,7 +2137,7 @@ def read_images(rawdata, colorspace, first_frame_only=False):
# msb-to-lsb: nothing to do
pass
elif fillorder == 2:
- logging.debug("fillorder is lsb-to-msb => reverse bits")
+ logger.debug("fillorder is lsb-to-msb => reverse bits")
# lsb-to-msb: reverse bits of each byte
rawdata = bytearray(rawdata)
for i in range(len(rawdata)):
@@ -1324,64 +2145,70 @@ def read_images(rawdata, colorspace, first_frame_only=False):
rawdata = bytes(rawdata)
else:
raise ValueError("unsupported FillOrder: %d" % fillorder)
- logging.debug("read_images() embeds Group4 from TIFF")
+ logger.debug("read_images() embeds Group4 from TIFF")
result.append(
(
color,
ndpi,
ImageFormat.CCITTGroup4,
rawdata,
+ None,
imgwidthpx,
imgheightpx,
[],
inverted,
1,
rotation,
+ iccp,
)
)
img_page_count += 1
continue
- logging.debug("Converting frame: %d" % img_page_count)
+ logger.debug("Converting frame: %d" % img_page_count)
- color, ndpi, imgwidthpx, imgheightpx, rotation = get_imgmetadata(
- imgdata, imgformat, default_dpi, colorspace
+ color, ndpi, imgwidthpx, imgheightpx, rotation, iccp = get_imgmetadata(
+ imgdata, imgformat, default_dpi, colorspace, rotreq=rot
)
newimg = None
if color == Colorspace["1"]:
try:
ccittdata = transcode_monochrome(imgdata)
- logging.debug("read_images() encoded a B/W image as CCITT group 4")
+ logger.debug("read_images() encoded a B/W image as CCITT group 4")
result.append(
(
color,
ndpi,
ImageFormat.CCITTGroup4,
ccittdata,
+ None,
imgwidthpx,
imgheightpx,
[],
False,
1,
rotation,
+ iccp,
)
)
img_page_count += 1
continue
except Exception as e:
- logging.debug(e)
- logging.debug("Converting colorspace 1 to L")
+ logger.debug(e)
+ logger.debug("Converting colorspace 1 to L")
newimg = imgdata.convert("L")
color = Colorspace.L
elif color in [
Colorspace.RGB,
+ Colorspace.RGBA,
Colorspace.L,
+ Colorspace.LA,
Colorspace.CMYK,
Colorspace["CMYK;I"],
Colorspace.P,
]:
- logging.debug("Colorspace is OK: %s", color)
+ logger.debug("Colorspace is OK: %s", color)
newimg = imgdata
else:
raise ValueError("unknown or unsupported colorspace: %s" % color.name)
@@ -1389,62 +2216,108 @@ def read_images(rawdata, colorspace, first_frame_only=False):
# compression
if color in [Colorspace.CMYK, Colorspace["CMYK;I"]]:
imggz = zlib.compress(newimg.tobytes())
- logging.debug("read_images() encoded CMYK with flate compression")
+ logger.debug("read_images() encoded CMYK with flate compression")
result.append(
(
color,
ndpi,
imgformat,
imggz,
+ None,
imgwidthpx,
imgheightpx,
[],
False,
8,
rotation,
+ iccp,
)
)
else:
- # cheapo version to retrieve a PNG encoding of the payload is to
- # just save it with PIL. In the future this could be replaced by
- # dedicated function applying the Paeth PNG filter to the raw pixel
- pngbuffer = BytesIO()
- newimg.save(pngbuffer, format="png")
- pngidat, palette = parse_png(pngbuffer.getvalue())
- # PIL does not provide the information about the original bits per
- # sample. Thus, we retrieve that info manually by looking at byte 9 in
- # the IHDR chunk. We know where to find that in the file because the
- # IHDR chunk must be the first chunk
- pngbuffer.seek(24)
- depth = ord(pngbuffer.read(1))
- if depth not in [1, 2, 4, 8, 16]:
- raise ValueError("invalid bit depth: %d" % depth)
- logging.debug("read_images() encoded an image as PNG")
+ if color in [Colorspace.P, Colorspace.PA] and iccp is not None:
+ # PDF does not support palette images with icc profile
+ if color == Colorspace.P:
+ newcolor = Colorspace.RGB
+ newimg = newimg.convert(mode="RGB")
+ elif color == Colorspace.PA:
+ newcolor = Colorspace.RGBA
+ newimg = newimg.convert(mode="RGBA")
+ smaskidat = None
+ elif (
+ color == Colorspace.RGBA
+ or color == Colorspace.LA
+ or color == Colorspace.PA
+ or "transparency" in newimg.info
+ ):
+ if color == Colorspace.RGBA:
+ newcolor = color
+ r, g, b, a = newimg.split()
+ newimg = Image.merge("RGB", (r, g, b))
+ elif color == Colorspace.LA:
+ newcolor = color
+ l, a = newimg.split()
+ newimg = l
+ elif color == Colorspace.PA or (
+ color == Colorspace.P and "transparency" in newimg.info
+ ):
+ newcolor = color
+ a = newimg.convert(mode="RGBA").split()[-1]
+ else:
+ newcolor = Colorspace.RGBA
+ r, g, b, a = newimg.convert(mode="RGBA").split()
+ newimg = Image.merge("RGB", (r, g, b))
+
+ smaskidat, *_ = to_png_data(a)
+ logger.warning(
+ "Image contains an alpha channel. Computing a separate "
+ "soft mask (/SMask) image to store transparency in PDF."
+ )
+ else:
+ newcolor = color
+ smaskidat = None
+
+ pngidat, palette, depth = to_png_data(newimg)
+ logger.debug("read_images() encoded an image as PNG")
result.append(
(
- color,
+ newcolor,
ndpi,
ImageFormat.PNG,
pngidat,
+ smaskidat,
imgwidthpx,
imgheightpx,
palette,
False,
depth,
rotation,
+ iccp,
)
)
img_page_count += 1
- # the python-pil version 2.3.0-1ubuntu3 in Ubuntu does not have the
- # close() method
- try:
- imgdata.close()
- except AttributeError:
- pass
- im.close()
+ cleanup()
return result
+def to_png_data(img):
+ # cheapo version to retrieve a PNG encoding of the payload is to
+ # just save it with PIL. In the future this could be replaced by
+ # dedicated function applying the Paeth PNG filter to the raw pixel
+ pngbuffer = BytesIO()
+ img.save(pngbuffer, format="png")
+
+ pngidat, palette = parse_png(pngbuffer.getvalue())
+ # PIL does not provide the information about the original bits per
+ # sample. Thus, we retrieve that info manually by looking at byte 9 in
+ # the IHDR chunk. We know where to find that in the file because the
+ # IHDR chunk must be the first chunk
+ pngbuffer.seek(24)
+ depth = ord(pngbuffer.read(1))
+ if depth not in [1, 2, 4, 8, 16]:
+ raise ValueError("invalid bit depth: %d" % depth)
+ return pngidat, palette, depth
+
+
# converts a length in pixels to a length in PDF units (1/72 of an inch)
def px_to_pt(length, dpi):
return 72.0 * length / dpi
@@ -1479,14 +2352,14 @@ def get_layout_fun(
and fitheight < 0
):
raise ValueError(
- "cannot fit into a rectangle where both " "dimensions are negative"
+ "cannot fit into a rectangle where both dimensions are negative"
)
elif fit not in [FitMode.fill, FitMode.enlarge] and (
(fitwidth is not None and fitwidth < 0)
or (fitheight is not None and fitheight < 0)
):
raise Exception(
- "cannot fit into a rectangle where either " "dimensions are negative"
+ "cannot fit into a rectangle where either dimensions are negative"
)
def default():
@@ -1719,7 +2592,11 @@ def get_fixed_dpi_layout_fun(fixed_dpi):
def find_scale(pagewidth, pageheight):
"""Find the power of 10 (10, 100, 1000...) that will reduce the scale
- below the PDF specification limit of 14400 PDF units (=200 inches)"""
+ below the PDF specification limit of 14400 PDF units (=200 inches).
+ In principle we could also choose a scale that is not a power of 10.
+ We use powers of 10 because numbers in the PDF format are represented
+ in base-10 and using powers of 10 will thus just shift the comma and
+ keep the numbers easily readable by humans as well."""
from math import log10, ceil
major = max(pagewidth, pageheight)
@@ -1736,8 +2613,8 @@ def find_scale(pagewidth, pageheight):
# as a binary string representing the image content or as filenames to the
# images.
def convert(*images, **kwargs):
-
_default_kwargs = dict(
+ engine=None,
title=None,
author=None,
creator=None,
@@ -1756,16 +2633,23 @@ def convert(*images, **kwargs):
viewer_fit_window=False,
viewer_center_window=False,
viewer_fullscreen=False,
- with_pdfrw=True,
outputstream=None,
first_frame_only=False,
allow_oversized=True,
+ cropborder=None,
+ bleedborder=None,
+ trimborder=None,
+ artborder=None,
+ pdfa=None,
+ rotation=None,
+ include_thumbnails=False,
)
for kwname, default in _default_kwargs.items():
if kwname not in kwargs:
kwargs[kwname] = default
pdf = pdfdoc(
+ kwargs["engine"],
"1.3",
kwargs["title"],
kwargs["author"],
@@ -1783,7 +2667,7 @@ def convert(*images, **kwargs):
kwargs["viewer_fit_window"],
kwargs["viewer_center_window"],
kwargs["viewer_fullscreen"],
- kwargs["with_pdfrw"],
+ kwargs["pdfa"],
)
# backwards compatibility with older img2pdf versions where the first
@@ -1795,46 +2679,72 @@ def convert(*images, **kwargs):
if not isinstance(images, (list, tuple)):
images = [images]
+ else:
+ if len(images) == 0:
+ raise ValueError("Unable to process empty list")
for img in images:
# img is allowed to be a path, a binary string representing image data
# or a file-like object (really anything that implements read())
- try:
- rawdata = img.read()
- except AttributeError:
+ # or a pathlib.Path object (really anything that implements read_bytes())
+ rawdata = None
+ for fun in "read", "read_bytes":
+ try:
+ rawdata = getattr(img, fun)()
+ except AttributeError:
+ pass
+ if rawdata is None:
if not isinstance(img, (str, bytes)):
- raise TypeError("Neither implements read() nor is str or bytes")
+ raise TypeError("Neither read(), read_bytes() nor is str or bytes")
# the thing doesn't have a read() function, so try if we can treat
# it as a file name
try:
- with open(img, "rb") as f:
- rawdata = f.read()
+ f = open(img, "rb")
except Exception:
# whatever the exception is (string could contain NUL
# characters or the path could just not exist) it's not a file
# name so we now try treating it as raw image content
rawdata = img
+ else:
+ # we are not using a "with" block here because we only want to
+ # catch exceptions thrown by open(). The read() may throw its
+ # own exceptions like MemoryError which should be handled
+ # differently.
+ rawdata = f.read()
+ f.close()
+
+ # md5 = hashlib.md5(rawdata).hexdigest()
+ # with open("./testdata/" + md5, "wb") as f:
+ # f.write(rawdata)
for (
color,
ndpi,
imgformat,
imgdata,
+ smaskdata,
imgwidthpx,
imgheightpx,
palette,
inverted,
depth,
rotation,
- ) in read_images(rawdata, kwargs["colorspace"], kwargs["first_frame_only"]):
+ iccp,
+ ) in read_images(
+ rawdata,
+ kwargs["colorspace"],
+ kwargs["first_frame_only"],
+ kwargs["rotation"],
+ kwargs["include_thumbnails"],
+ ):
pagewidth, pageheight, imgwidthpdf, imgheightpdf = kwargs["layout_fun"](
imgwidthpx, imgheightpx, ndpi
)
userunit = None
if pagewidth < 3.00 or pageheight < 3.00:
- logging.warning(
- "pdf width or height is below 3.00 - too " "small for some viewers!"
+ logger.warning(
+ "pdf width or height is below 3.00 - too small for some viewers!"
)
elif pagewidth > 14400.0 or pageheight > 14400.0:
if kwargs["allow_oversized"]:
@@ -1847,6 +2757,17 @@ def convert(*images, **kwargs):
raise PdfTooLargeError(
"pdf width or height must not exceed 200 inches."
)
+ for border in ["crop", "bleed", "trim", "art"]:
+ if kwargs[border + "border"] is None:
+ continue
+ if pagewidth < 2 * kwargs[border + "border"][1]:
+ raise ValueError(
+ "horizontal %s border larger than page width" % border
+ )
+ if pageheight < 2 * kwargs[border + "border"][0]:
+ raise ValueError(
+ "vertical %s border larger than page height" % border
+ )
# the image is always centered on the page
imgxpdf = (pagewidth - imgwidthpdf) / 2.0
imgypdf = (pageheight - imgheightpdf) / 2.0
@@ -1856,6 +2777,7 @@ def convert(*images, **kwargs):
imgheightpx,
imgformat,
imgdata,
+ smaskdata,
imgwidthpdf,
imgheightpdf,
imgxpdf,
@@ -1867,6 +2789,11 @@ def convert(*images, **kwargs):
inverted,
depth,
rotation,
+ kwargs["cropborder"],
+ kwargs["bleedborder"],
+ kwargs["trimborder"],
+ kwargs["artborder"],
+ iccp,
)
if kwargs["outputstream"]:
@@ -1906,6 +2833,9 @@ def parse_num(num, name):
except ValueError:
msg = "%s is not a floating point number: %s" % (name, num)
raise argparse.ArgumentTypeError(msg)
+ if num < 0:
+ msg = "%s must not be negative: %s" % (name, num)
+ raise argparse.ArgumentTypeError(msg)
if unit == Unit.cm:
num = cm_to_pt(num)
elif unit == Unit.mm:
@@ -1988,7 +2918,7 @@ def parse_pagesize_rectarg(string):
if transposed:
w, h = h, w
if w is None and h is None:
- raise argparse.ArgumentTypeError("at least one dimension must be " "specified")
+ raise argparse.ArgumentTypeError("at least one dimension must be specified")
return w, h
@@ -2010,7 +2940,7 @@ def parse_imgsize_rectarg(string):
if transposed:
w, h = h, w
if w is None and h is None:
- raise argparse.ArgumentTypeError("at least one dimension must be " "specified")
+ raise argparse.ArgumentTypeError("at least one dimension must be specified")
return w, h
@@ -2020,7 +2950,17 @@ def parse_colorspacearg(string):
return c
allowed = ", ".join([c.name for c in Colorspace])
raise argparse.ArgumentTypeError(
- "Unsupported colorspace: %s. Must be one " "of: %s." % (string, allowed)
+ "Unsupported colorspace: %s. Must be one of: %s." % (string, allowed)
+ )
+
+
+def parse_enginearg(string):
+ for c in Engine:
+ if c.name == string:
+ return c
+ allowed = ", ".join([c.name for c in Engine])
+ raise argparse.ArgumentTypeError(
+ "Unsupported engine: %s. Must be one of: %s." % (string, allowed)
)
@@ -2041,17 +2981,48 @@ def parse_borderarg(string):
return h, v
-def input_images(path):
+def from_file(path):
+ result = []
if path == "-":
+ content = sys.stdin.buffer.read()
+ else:
+ with open(path, "rb") as f:
+ content = f.read()
+ for path in content.split(b"\0"):
+ if path == b"":
+ continue
+ try:
+ # test-read a byte from it so that we can abort early in case
+ # we cannot read data from the file
+ with open(path, "rb") as im:
+ im.read(1)
+ except IsADirectoryError:
+ raise argparse.ArgumentTypeError('"%s" is a directory' % path)
+ except PermissionError:
+ raise argparse.ArgumentTypeError('"%s" permission denied' % path)
+ except FileNotFoundError:
+ raise argparse.ArgumentTypeError('"%s" does not exist' % path)
+ result.append(path)
+ return result
+
+
+def input_images(path_expr):
+ if path_expr == "-":
# we slurp in all data from stdin because we need to seek in it later
- if PY3:
- result = sys.stdin.buffer.read()
- else:
- result = sys.stdin.read()
+ result = [sys.stdin.buffer.read()]
if len(result) == 0:
- raise argparse.ArgumentTypeError('"%s" is empty' % path)
+ raise argparse.ArgumentTypeError('"%s" is empty' % path_expr)
else:
- if PY3:
+ result = []
+ paths = [path_expr]
+ if sys.platform == "win32" and ("*" in path_expr or "?" in path_expr):
+ # on windows, program is responsible for expanding wildcards such as *.jpg
+ # glob won't return files that don't exist so we only use it for wildcards
+ # paths without wildcards that do not exist will trigger "does not exist"
+ from glob import glob
+
+ paths = sorted(glob(path_expr))
+ for path in paths:
try:
if os.path.getsize(path) == 0:
raise argparse.ArgumentTypeError('"%s" is empty' % path)
@@ -2065,22 +3036,17 @@ def input_images(path):
raise argparse.ArgumentTypeError('"%s" permission denied' % path)
except FileNotFoundError:
raise argparse.ArgumentTypeError('"%s" does not exist' % path)
- else:
- try:
- if os.path.getsize(path) == 0:
- raise argparse.ArgumentTypeError('"%s" is empty' % path)
- # test-read a byte from it so that we can abort early in case
- # we cannot read data from the file
- with open(path, "rb") as im:
- im.read(1)
- except IOError as err:
- raise argparse.ArgumentTypeError(str(err))
- except OSError as err:
- raise argparse.ArgumentTypeError(str(err))
- result = path
+ result.append(path)
return result
+def parse_rotationarg(string):
+ for m in Rotation:
+ if m.name == string.lower():
+ return m
+ raise argparse.ArgumentTypeError("unknown rotation value: %s" % string)
+
+
def parse_fitarg(string):
for m in FitMode:
if m.name == string.lower():
@@ -2094,7 +3060,7 @@ def parse_panes(string):
return m
allowed = ", ".join([m.name for m in PageMode])
raise argparse.ArgumentTypeError(
- "Unsupported page mode: %s. Must be one " "of: %s." % (string, allowed)
+ "Unsupported page mode: %s. Must be one of: %s." % (string, allowed)
)
@@ -2119,7 +3085,7 @@ def parse_layout(string):
return l
allowed = ", ".join([l.name for l in PageLayout])
raise argparse.ArgumentTypeError(
- "Unsupported page layout: %s. Must be " "one of: %s." % (string, allowed)
+ "Unsupported page layout: %s. Must be one of: %s." % (string, allowed)
)
@@ -2145,7 +3111,7 @@ def valid_date(string):
else:
try:
return parser.parse(string)
- except TypeError:
+ except:
pass
# as a last resort, try the local date utility
try:
@@ -2158,11 +3124,737 @@ def valid_date(string):
except subprocess.CalledProcessError:
pass
else:
- return datetime.utcfromtimestamp(int(utime))
+ return datetime.fromtimestamp(int(utime))
raise argparse.ArgumentTypeError("cannot parse date: %s" % string)
-def main(argv=sys.argv):
+def gui():
+ import tkinter
+ import tkinter.filedialog
+
+ have_fitz = True
+ try:
+ import fitz
+ except ImportError:
+ have_fitz = False
+
+ # from Python 3.7 Lib/idlelib/configdialog.py
+ # Copyright 2015-2017 Terry Jan Reedy
+ # Python License
+ class VerticalScrolledFrame(tkinter.Frame):
+ """A pure Tkinter vertically scrollable frame.
+
+ * Use the 'interior' attribute to place widgets inside the scrollable frame
+ * Construct and pack/place/grid normally
+ * This frame only allows vertical scrolling
+ """
+
+ def __init__(self, parent, *args, **kw):
+ tkinter.Frame.__init__(self, parent, *args, **kw)
+
+ # Create a canvas object and a vertical scrollbar for scrolling it.
+ vscrollbar = tkinter.Scrollbar(self, orient=tkinter.VERTICAL)
+ vscrollbar.pack(fill=tkinter.Y, side=tkinter.RIGHT, expand=tkinter.FALSE)
+ canvas = tkinter.Canvas(
+ self,
+ borderwidth=0,
+ highlightthickness=0,
+ yscrollcommand=vscrollbar.set,
+ width=240,
+ )
+ canvas.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=tkinter.TRUE)
+ vscrollbar.config(command=canvas.yview)
+
+ # Reset the view.
+ canvas.xview_moveto(0)
+ canvas.yview_moveto(0)
+
+ # Create a frame inside the canvas which will be scrolled with it.
+ self.interior = interior = tkinter.Frame(canvas)
+ interior_id = canvas.create_window(0, 0, window=interior, anchor=tkinter.NW)
+
+ # Track changes to the canvas and frame width and sync them,
+ # also updating the scrollbar.
+ def _configure_interior(event):
+ # Update the scrollbars to match the size of the inner frame.
+ size = (interior.winfo_reqwidth(), interior.winfo_reqheight())
+ canvas.config(scrollregion="0 0 %s %s" % size)
+
+ interior.bind("<Configure>", _configure_interior)
+
+ def _configure_canvas(event):
+ if interior.winfo_reqwidth() != canvas.winfo_width():
+ # Update the inner frame's width to fill the canvas.
+ canvas.itemconfigure(interior_id, width=canvas.winfo_width())
+
+ canvas.bind("<Configure>", _configure_canvas)
+
+ return
+
+ # From Python 3.7 Lib/tkinter/__init__.py
+ # Copyright 2000 Fredrik Lundh
+ # Python License
+ #
+ # add support for 'state' and 'name' kwargs
+ # add support for updating list of options
+ class OptionMenu(tkinter.Menubutton):
+ """OptionMenu which allows the user to select a value from a menu."""
+
+ def __init__(self, master, variable, value, *values, **kwargs):
+ """Construct an optionmenu widget with the parent MASTER, with
+ the resource textvariable set to VARIABLE, the initially selected
+ value VALUE, the other menu values VALUES and an additional
+ keyword argument command."""
+ kw = {
+ "borderwidth": 2,
+ "textvariable": variable,
+ "indicatoron": 1,
+ "relief": tkinter.RAISED,
+ "anchor": "c",
+ "highlightthickness": 2,
+ }
+ if "state" in kwargs:
+ kw["state"] = kwargs["state"]
+ del kwargs["state"]
+ if "name" in kwargs:
+ kw["name"] = kwargs["name"]
+ del kwargs["name"]
+ tkinter.Widget.__init__(self, master, "menubutton", kw)
+ self.widgetName = "tk_optionMenu"
+ self.callback = kwargs.get("command")
+ self.variable = variable
+ if "command" in kwargs:
+ del kwargs["command"]
+ if kwargs:
+ raise tkinter.TclError("unknown option -" + list(kwargs.keys())[0])
+ self.set_values([value] + list(values))
+
+ def __getitem__(self, name):
+ if name == "menu":
+ return self.__menu
+ return tkinter.Widget.__getitem__(self, name)
+
+ def set_values(self, values):
+ menu = self.__menu = tkinter.Menu(self, name="menu", tearoff=0)
+ self.menuname = menu._w
+ for v in values:
+ menu.add_command(
+ label=v, command=tkinter._setit(self.variable, v, self.callback)
+ )
+ self["menu"] = menu
+
+ def destroy(self):
+ """Destroy this widget and the associated menu."""
+ tkinter.Menubutton.destroy(self)
+ self.__menu = None
+
+ root = tkinter.Tk()
+ app = tkinter.Frame(master=root)
+
+ infiles = []
+ maxpagewidth = 0
+ maxpageheight = 0
+ doc = None
+
+ args = {
+ "engine": tkinter.StringVar(),
+ "auto_orient": tkinter.BooleanVar(),
+ "fit": tkinter.StringVar(),
+ "title": tkinter.StringVar(),
+ "author": tkinter.StringVar(),
+ "creator": tkinter.StringVar(),
+ "producer": tkinter.StringVar(),
+ "subject": tkinter.StringVar(),
+ "keywords": tkinter.StringVar(),
+ "nodate": tkinter.BooleanVar(),
+ "creationdate": tkinter.StringVar(),
+ "moddate": tkinter.StringVar(),
+ "viewer_panes": tkinter.StringVar(),
+ "viewer_initial_page": tkinter.IntVar(),
+ "viewer_magnification": tkinter.StringVar(),
+ "viewer_page_layout": tkinter.StringVar(),
+ "viewer_fit_window": tkinter.BooleanVar(),
+ "viewer_center_window": tkinter.BooleanVar(),
+ "viewer_fullscreen": tkinter.BooleanVar(),
+ "pagesize_dropdown": tkinter.StringVar(),
+ "pagesize_width": tkinter.DoubleVar(),
+ "pagesize_height": tkinter.DoubleVar(),
+ "imgsize_dropdown": tkinter.StringVar(),
+ "imgsize_width": tkinter.DoubleVar(),
+ "imgsize_height": tkinter.DoubleVar(),
+ "colorspace": tkinter.StringVar(),
+ "first_frame_only": tkinter.BooleanVar(),
+ }
+ args["engine"].set("auto")
+ args["title"].set("")
+ args["auto_orient"].set(False)
+ args["fit"].set("into")
+ args["colorspace"].set("auto")
+ args["viewer_panes"].set("auto")
+ args["viewer_initial_page"].set(1)
+ args["viewer_magnification"].set("auto")
+ args["viewer_page_layout"].set("auto")
+ args["first_frame_only"].set(False)
+ args["pagesize_dropdown"].set("auto")
+ args["imgsize_dropdown"].set("auto")
+
+ def on_open_button():
+ nonlocal infiles
+ nonlocal doc
+ nonlocal maxpagewidth
+ nonlocal maxpageheight
+ infiles = tkinter.filedialog.askopenfilenames(
+ parent=root,
+ title="open image",
+ filetypes=[
+ (
+ "images",
+ "*.bmp *.eps *.gif *.ico *.jpeg *.jpg *.jp2 *.pcx *.png *.ppm *.tiff",
+ ),
+ ("all files", "*"),
+ ],
+ # initialdir="/home/josch/git/plakativ",
+ # initialfile="test.pdf",
+ )
+ if have_fitz:
+ with BytesIO() as f:
+ save_pdf(f)
+ f.seek(0)
+ doc = fitz.open(stream=f, filetype="pdf")
+ for page in doc:
+ if page.getDisplayList().rect.width > maxpagewidth:
+ maxpagewidth = page.getDisplayList().rect.width
+ if page.getDisplayList().rect.height > maxpageheight:
+ maxpageheight = page.getDisplayList().rect.height
+ draw()
+
+ def save_pdf(stream):
+ pagesizearg = None
+ if args["pagesize_dropdown"].get() == "auto":
+ # nothing to do
+ pass
+ elif args["pagesize_dropdown"].get() == "custom":
+ pagesizearg = args["pagesize_width"].get(), args["pagesize_height"].get()
+ elif args["pagesize_dropdown"].get() in papernames.values():
+ raise NotImplemented()
+ else:
+ raise Exception("no such pagesize: %s" % args["pagesize_dropdown"].get())
+ imgsizearg = None
+ if args["imgsize_dropdown"].get() == "auto":
+ # nothing to do
+ pass
+ elif args["imgsize_dropdown"].get() == "custom":
+ imgsizearg = args["imgsize_width"].get(), args["imgsize_height"].get()
+ elif args["imgsize_dropdown"].get() in papernames.values():
+ raise NotImplemented()
+ else:
+ raise Exception("no such imgsize: %s" % args["imgsize_dropdown"].get())
+ borderarg = None
+ layout_fun = get_layout_fun(
+ pagesizearg,
+ imgsizearg,
+ borderarg,
+ args["fit"].get(),
+ args["auto_orient"].get(),
+ )
+ viewer_panesarg = None
+ if args["viewer_panes"].get() == "auto":
+ # nothing to do
+ pass
+ elif args["viewer_panes"].get() in PageMode:
+ viewer_panesarg = args["viewer_panes"].get()
+ else:
+ raise Exception("no such viewer_panes: %s" % args["viewer_panes"].get())
+ viewer_magnificationarg = None
+ if args["viewer_magnification"].get() == "auto":
+ # nothing to do
+ pass
+ elif args["viewer_magnification"].get() in Magnification:
+ viewer_magnificationarg = args["viewer_magnification"].get()
+ else:
+ raise Exception(
+ "no such viewer_magnification: %s" % args["viewer_magnification"].get()
+ )
+ viewer_page_layoutarg = None
+ if args["viewer_page_layout"].get() == "auto":
+ # nothing to do
+ pass
+ elif args["viewer_page_layout"].get() in PageLayout:
+ viewer_page_layoutarg = args["viewer_page_layout"].get()
+ else:
+ raise Exception(
+ "no such viewer_page_layout: %s" % args["viewer_page_layout"].get()
+ )
+ colorspacearg = None
+ if args["colorspace"].get() != "auto":
+ colorspacearg = next(
+ v for v in Colorspace if v.name == args["colorspace"].get()
+ )
+ enginearg = None
+ if args["engine"].get() != "auto":
+ enginearg = next(v for v in Engine if v.name == args["engine"].get())
+
+ convert(
+ *infiles,
+ engine=enginearg,
+ title=args["title"].get() if args["title"].get() else None,
+ author=args["author"].get() if args["author"].get() else None,
+ creator=args["creator"].get() if args["creator"].get() else None,
+ producer=args["producer"].get() if args["producer"].get() else None,
+ creationdate=args["creationdate"].get()
+ if args["creationdate"].get()
+ else None,
+ moddate=args["moddate"].get() if args["moddate"].get() else None,
+ subject=args["subject"].get() if args["subject"].get() else None,
+ keywords=args["keywords"].get() if args["keywords"].get() else None,
+ colorspace=colorspacearg,
+ nodate=args["nodate"].get(),
+ layout_fun=layout_fun,
+ viewer_panes=viewer_panesarg,
+ viewer_initial_page=args["viewer_initial_page"].get()
+ if args["viewer_initial_page"].get() > 1
+ else None,
+ viewer_magnification=viewer_magnificationarg,
+ viewer_page_layout=viewer_page_layoutarg,
+ viewer_fit_window=(args["viewer_fit_window"].get() or None),
+ viewer_center_window=(args["viewer_center_window"].get() or None),
+ viewer_fullscreen=(args["viewer_fullscreen"].get() or None),
+ outputstream=stream,
+ first_frame_only=args["first_frame_only"].get(),
+ cropborder=None,
+ bleedborder=None,
+ trimborder=None,
+ artborder=None,
+ )
+
+ def on_save_button():
+ filename = tkinter.filedialog.asksaveasfilename(
+ parent=root,
+ title="save PDF",
+ defaultextension=".pdf",
+ filetypes=[("pdf documents", "*.pdf"), ("all files", "*")],
+ # initialdir="/home/josch/git/plakativ",
+ # initialfile=base + "_poster" + ext,
+ )
+ with open(filename, "wb") as f:
+ save_pdf(f)
+
+ root.title("img2pdf")
+ app.pack(fill=tkinter.BOTH, expand=tkinter.TRUE)
+
+ canvas = tkinter.Canvas(app, bg="black")
+
+ def draw():
+ canvas.delete(tkinter.ALL)
+ if not infiles:
+ canvas.create_text(
+ canvas.size[0] / 2,
+ canvas.size[1] / 2,
+ text='Click on the "Open Image(s)" button in the upper right.',
+ fill="white",
+ )
+ return
+
+ if not doc:
+ canvas.create_text(
+ canvas.size[0] / 2,
+ canvas.size[1] / 2,
+ text="PyMuPDF not available. Install the Python fitz module\n"
+ + "for preview functionality.",
+ fill="white",
+ )
+ return
+
+ canvas_padding = 10
+ # factor to convert from pdf dimensions (given in pt) into canvas
+ # dimensions (given in pixels)
+ zoom = min(
+ (canvas.size[0] - canvas_padding) / maxpagewidth,
+ (canvas.size[1] - canvas_padding) / maxpageheight,
+ )
+
+ pagenum = 0
+ mat_0 = fitz.Matrix(zoom, zoom)
+ canvas.image = tkinter.PhotoImage(
+ data=doc[pagenum]
+ .getDisplayList()
+ .getPixmap(matrix=mat_0, alpha=False)
+ .getImageData("ppm")
+ )
+ canvas.create_image(
+ (canvas.size[0] - maxpagewidth * zoom) / 2,
+ (canvas.size[1] - maxpageheight * zoom) / 2,
+ anchor=tkinter.NW,
+ image=canvas.image,
+ )
+
+ canvas.create_rectangle(
+ (canvas.size[0] - maxpagewidth * zoom) / 2,
+ (canvas.size[1] - maxpageheight * zoom) / 2,
+ (canvas.size[0] - maxpagewidth * zoom) / 2 + canvas.image.width(),
+ (canvas.size[1] - maxpageheight * zoom) / 2 + canvas.image.height(),
+ outline="red",
+ )
+
+ def on_resize(event):
+ canvas.size = (event.width, event.height)
+ draw()
+
+ canvas.pack(fill=tkinter.BOTH, side=tkinter.LEFT, expand=tkinter.TRUE)
+ canvas.bind("<Configure>", on_resize)
+
+ frame_right = tkinter.Frame(app)
+ frame_right.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.Y)
+
+ top_frame = tkinter.Frame(frame_right)
+ top_frame.pack(fill=tkinter.X)
+
+ tkinter.Button(top_frame, text="Open Image(s)", command=on_open_button).pack(
+ side=tkinter.LEFT, expand=tkinter.TRUE, fill=tkinter.X
+ )
+ tkinter.Button(top_frame, text="Help", state=tkinter.DISABLED).pack(
+ side=tkinter.RIGHT, expand=tkinter.TRUE, fill=tkinter.X
+ )
+
+ frame1 = VerticalScrolledFrame(frame_right)
+ frame1.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.Y)
+
+ output_options = tkinter.LabelFrame(frame1.interior, text="Output Options")
+ output_options.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
+ tkinter.Label(output_options, text="colorspace").grid(
+ row=0, column=0, sticky=tkinter.W
+ )
+ OptionMenu(output_options, args["colorspace"], "auto", state=tkinter.DISABLED).grid(
+ row=0, column=1, sticky=tkinter.W
+ )
+ tkinter.Label(output_options, text="engine").grid(row=1, column=0, sticky=tkinter.W)
+ OptionMenu(output_options, args["engine"], "auto", state=tkinter.DISABLED).grid(
+ row=1, column=1, sticky=tkinter.W
+ )
+ tkinter.Checkbutton(
+ output_options,
+ text="Suppress timestamp",
+ variable=args["nodate"],
+ state=tkinter.DISABLED,
+ ).grid(row=2, column=0, columnspan=2, sticky=tkinter.W)
+ tkinter.Checkbutton(
+ output_options,
+ text="only first frame",
+ variable=args["first_frame_only"],
+ state=tkinter.DISABLED,
+ ).grid(row=3, column=0, columnspan=2, sticky=tkinter.W)
+ tkinter.Checkbutton(
+ output_options, text="force large input", state=tkinter.DISABLED
+ ).grid(row=4, column=0, columnspan=2, sticky=tkinter.W)
+ image_size_frame = tkinter.LabelFrame(frame1.interior, text="Image size")
+ image_size_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
+ OptionMenu(
+ image_size_frame,
+ args["imgsize_dropdown"],
+ *(["auto", "custom"] + sorted(papernames.values())),
+ state=tkinter.DISABLED,
+ ).grid(row=1, column=0, columnspan=3, sticky=tkinter.W)
+
+ tkinter.Label(
+ image_size_frame, text="Width:", state=tkinter.DISABLED, name="size_label_width"
+ ).grid(row=2, column=0, sticky=tkinter.W)
+ tkinter.Spinbox(
+ image_size_frame,
+ format="%.2f",
+ increment=0.01,
+ from_=0,
+ to=100,
+ width=5,
+ state=tkinter.DISABLED,
+ name="spinbox_width",
+ ).grid(row=2, column=1, sticky=tkinter.W)
+ tkinter.Label(
+ image_size_frame, text="mm", state=tkinter.DISABLED, name="size_label_width_mm"
+ ).grid(row=2, column=2, sticky=tkinter.W)
+
+ tkinter.Label(
+ image_size_frame,
+ text="Height:",
+ state=tkinter.DISABLED,
+ name="size_label_height",
+ ).grid(row=3, column=0, sticky=tkinter.W)
+ tkinter.Spinbox(
+ image_size_frame,
+ format="%.2f",
+ increment=0.01,
+ from_=0,
+ to=100,
+ width=5,
+ state=tkinter.DISABLED,
+ name="spinbox_height",
+ ).grid(row=3, column=1, sticky=tkinter.W)
+ tkinter.Label(
+ image_size_frame, text="mm", state=tkinter.DISABLED, name="size_label_height_mm"
+ ).grid(row=3, column=2, sticky=tkinter.W)
+
+ page_size_frame = tkinter.LabelFrame(frame1.interior, text="Page size")
+ page_size_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
+ OptionMenu(
+ page_size_frame,
+ args["pagesize_dropdown"],
+ *(["auto", "custom"] + sorted(papernames.values())),
+ state=tkinter.DISABLED,
+ ).grid(row=1, column=0, columnspan=3, sticky=tkinter.W)
+
+ tkinter.Label(
+ page_size_frame, text="Width:", state=tkinter.DISABLED, name="size_label_width"
+ ).grid(row=2, column=0, sticky=tkinter.W)
+ tkinter.Spinbox(
+ page_size_frame,
+ format="%.2f",
+ increment=0.01,
+ from_=0,
+ to=100,
+ width=5,
+ state=tkinter.DISABLED,
+ name="spinbox_width",
+ ).grid(row=2, column=1, sticky=tkinter.W)
+ tkinter.Label(
+ page_size_frame, text="mm", state=tkinter.DISABLED, name="size_label_width_mm"
+ ).grid(row=2, column=2, sticky=tkinter.W)
+
+ tkinter.Label(
+ page_size_frame,
+ text="Height:",
+ state=tkinter.DISABLED,
+ name="size_label_height",
+ ).grid(row=3, column=0, sticky=tkinter.W)
+ tkinter.Spinbox(
+ page_size_frame,
+ format="%.2f",
+ increment=0.01,
+ from_=0,
+ to=100,
+ width=5,
+ state=tkinter.DISABLED,
+ name="spinbox_height",
+ ).grid(row=3, column=1, sticky=tkinter.W)
+ tkinter.Label(
+ page_size_frame, text="mm", state=tkinter.DISABLED, name="size_label_height_mm"
+ ).grid(row=3, column=2, sticky=tkinter.W)
+ layout_frame = tkinter.LabelFrame(frame1.interior, text="Layout")
+ layout_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
+ tkinter.Label(layout_frame, text="border", state=tkinter.DISABLED).grid(
+ row=0, column=0, sticky=tkinter.W
+ )
+ tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
+ row=0, column=1, sticky=tkinter.W
+ )
+ tkinter.Label(layout_frame, text="fit", state=tkinter.DISABLED).grid(
+ row=1, column=0, sticky=tkinter.W
+ )
+ OptionMenu(
+ layout_frame, args["fit"], *[v.name for v in FitMode], state=tkinter.DISABLED
+ ).grid(row=1, column=1, sticky=tkinter.W)
+ tkinter.Checkbutton(
+ layout_frame,
+ text="auto orient",
+ state=tkinter.DISABLED,
+ variable=args["auto_orient"],
+ ).grid(row=2, column=0, columnspan=2, sticky=tkinter.W)
+ tkinter.Label(layout_frame, text="crop border", state=tkinter.DISABLED).grid(
+ row=3, column=0, sticky=tkinter.W
+ )
+ tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
+ row=3, column=1, sticky=tkinter.W
+ )
+ tkinter.Label(layout_frame, text="bleed border", state=tkinter.DISABLED).grid(
+ row=4, column=0, sticky=tkinter.W
+ )
+ tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
+ row=4, column=1, sticky=tkinter.W
+ )
+ tkinter.Label(layout_frame, text="trim border", state=tkinter.DISABLED).grid(
+ row=5, column=0, sticky=tkinter.W
+ )
+ tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
+ row=5, column=1, sticky=tkinter.W
+ )
+ tkinter.Label(layout_frame, text="art border", state=tkinter.DISABLED).grid(
+ row=6, column=0, sticky=tkinter.W
+ )
+ tkinter.Spinbox(layout_frame, state=tkinter.DISABLED).grid(
+ row=6, column=1, sticky=tkinter.W
+ )
+ metadata_frame = tkinter.LabelFrame(frame1.interior, text="PDF metadata")
+ metadata_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
+ tkinter.Label(metadata_frame, text="title", state=tkinter.DISABLED).grid(
+ row=0, column=0, sticky=tkinter.W
+ )
+ tkinter.Entry(
+ metadata_frame, textvariable=args["title"], state=tkinter.DISABLED
+ ).grid(row=0, column=1, sticky=tkinter.W)
+ tkinter.Label(metadata_frame, text="author", state=tkinter.DISABLED).grid(
+ row=1, column=0, sticky=tkinter.W
+ )
+ tkinter.Entry(
+ metadata_frame, textvariable=args["author"], state=tkinter.DISABLED
+ ).grid(row=1, column=1, sticky=tkinter.W)
+ tkinter.Label(metadata_frame, text="creator", state=tkinter.DISABLED).grid(
+ row=2, column=0, sticky=tkinter.W
+ )
+ tkinter.Entry(
+ metadata_frame, textvariable=args["creator"], state=tkinter.DISABLED
+ ).grid(row=2, column=1, sticky=tkinter.W)
+ tkinter.Label(metadata_frame, text="producer", state=tkinter.DISABLED).grid(
+ row=3, column=0, sticky=tkinter.W
+ )
+ tkinter.Entry(
+ metadata_frame, textvariable=args["producer"], state=tkinter.DISABLED
+ ).grid(row=3, column=1, sticky=tkinter.W)
+ tkinter.Label(metadata_frame, text="creation date", state=tkinter.DISABLED).grid(
+ row=4, column=0, sticky=tkinter.W
+ )
+ tkinter.Entry(
+ metadata_frame, textvariable=args["creationdate"], state=tkinter.DISABLED
+ ).grid(row=4, column=1, sticky=tkinter.W)
+ tkinter.Label(
+ metadata_frame, text="modification date", state=tkinter.DISABLED
+ ).grid(row=5, column=0, sticky=tkinter.W)
+ tkinter.Entry(
+ metadata_frame, textvariable=args["moddate"], state=tkinter.DISABLED
+ ).grid(row=5, column=1, sticky=tkinter.W)
+ tkinter.Label(metadata_frame, text="subject", state=tkinter.DISABLED).grid(
+ row=6, column=0, sticky=tkinter.W
+ )
+ tkinter.Entry(metadata_frame, state=tkinter.DISABLED).grid(
+ row=6, column=1, sticky=tkinter.W
+ )
+ tkinter.Label(metadata_frame, text="keywords", state=tkinter.DISABLED).grid(
+ row=7, column=0, sticky=tkinter.W
+ )
+ tkinter.Entry(metadata_frame, state=tkinter.DISABLED).grid(
+ row=7, column=1, sticky=tkinter.W
+ )
+ viewer_frame = tkinter.LabelFrame(frame1.interior, text="PDF viewer options")
+ viewer_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
+ tkinter.Label(viewer_frame, text="panes", state=tkinter.DISABLED).grid(
+ row=0, column=0, sticky=tkinter.W
+ )
+ OptionMenu(
+ viewer_frame,
+ args["viewer_panes"],
+ *(["auto"] + [v.name for v in PageMode]),
+ state=tkinter.DISABLED,
+ ).grid(row=0, column=1, sticky=tkinter.W)
+ tkinter.Label(viewer_frame, text="initial page", state=tkinter.DISABLED).grid(
+ row=1, column=0, sticky=tkinter.W
+ )
+ tkinter.Spinbox(
+ viewer_frame,
+ increment=1,
+ from_=1,
+ to=10000,
+ width=6,
+ textvariable=args["viewer_initial_page"],
+ state=tkinter.DISABLED,
+ name="viewer_initial_page_spinbox",
+ ).grid(row=1, column=1, sticky=tkinter.W)
+ tkinter.Label(viewer_frame, text="magnification", state=tkinter.DISABLED).grid(
+ row=2, column=0, sticky=tkinter.W
+ )
+ OptionMenu(
+ viewer_frame,
+ args["viewer_magnification"],
+ *(["auto", "custom"] + [v.name for v in Magnification]),
+ state=tkinter.DISABLED,
+ ).grid(row=2, column=1, sticky=tkinter.W)
+ tkinter.Label(viewer_frame, text="page layout", state=tkinter.DISABLED).grid(
+ row=3, column=0, sticky=tkinter.W
+ )
+ OptionMenu(
+ viewer_frame,
+ args["viewer_page_layout"],
+ *(["auto"] + [v.name for v in PageLayout]),
+ state=tkinter.DISABLED,
+ ).grid(row=3, column=1, sticky=tkinter.W)
+ tkinter.Checkbutton(
+ viewer_frame,
+ text="fit window to page size",
+ variable=args["viewer_fit_window"],
+ state=tkinter.DISABLED,
+ ).grid(row=4, column=0, columnspan=2, sticky=tkinter.W)
+ tkinter.Checkbutton(
+ viewer_frame,
+ text="center window",
+ variable=args["viewer_center_window"],
+ state=tkinter.DISABLED,
+ ).grid(row=5, column=0, columnspan=2, sticky=tkinter.W)
+ tkinter.Checkbutton(
+ viewer_frame,
+ text="open in fullscreen",
+ variable=args["viewer_fullscreen"],
+ state=tkinter.DISABLED,
+ ).grid(row=6, column=0, columnspan=2, sticky=tkinter.W)
+
+ option_frame = tkinter.LabelFrame(frame1.interior, text="Program options")
+ option_frame.pack(side=tkinter.TOP, expand=tkinter.TRUE, fill=tkinter.X)
+
+ tkinter.Label(option_frame, text="Unit:", state=tkinter.DISABLED).grid(
+ row=0, column=0, sticky=tkinter.W
+ )
+ unit = tkinter.StringVar()
+ unit.set("mm")
+ OptionMenu(option_frame, unit, ["mm"], state=tkinter.DISABLED).grid(
+ row=0, column=1, sticky=tkinter.W
+ )
+
+ tkinter.Label(option_frame, text="Language:", state=tkinter.DISABLED).grid(
+ row=1, column=0, sticky=tkinter.W
+ )
+ language = tkinter.StringVar()
+ language.set("English")
+ OptionMenu(option_frame, language, ["English"], state=tkinter.DISABLED).grid(
+ row=1, column=1, sticky=tkinter.W
+ )
+
+ bottom_frame = tkinter.Frame(frame_right)
+ bottom_frame.pack(fill=tkinter.X)
+
+ tkinter.Button(bottom_frame, text="Save PDF", command=on_save_button).pack(
+ side=tkinter.LEFT, expand=tkinter.TRUE, fill=tkinter.X
+ )
+ tkinter.Button(bottom_frame, text="Exit", command=root.destroy).pack(
+ side=tkinter.RIGHT, expand=tkinter.TRUE, fill=tkinter.X
+ )
+
+ app.mainloop()
+
+
+def file_is_icc(fname):
+ with open(fname, "rb") as f:
+ data = f.read(40)
+ if len(data) < 40:
+ return False
+ return data[36:] == b"acsp"
+
+
+def validate_icc(fname):
+ if not file_is_icc(fname):
+ raise argparse.ArgumentTypeError('"%s" is not an ICC profile' % fname)
+ return fname
+
+
+def get_default_icc_profile():
+ for profile in [
+ "/usr/share/color/icc/sRGB.icc",
+ "/usr/share/color/icc/OpenICC/sRGB.icc",
+ "/usr/share/color/icc/colord/sRGB.icc",
+ ]:
+ if not os.path.exists(profile):
+ continue
+ if not file_is_icc(profile):
+ continue
+ return profile
+ return "/usr/share/color/icc/sRGB.icc"
+
+
+def get_main_parser():
rendered_papersizes = ""
for k, v in sorted(papersizes.items()):
rendered_papersizes += " %-8s %s\n" % (papernames[k], v)
@@ -2174,9 +3866,9 @@ Losslessly convert raster images to PDF without re-encoding PNG, JPEG, and
JPEG2000 images. This leads to a lossless conversion of PNG, JPEG and JPEG2000
images with the only added file size coming from the PDF container itself.
Other raster graphics formats are losslessly stored using the same encoding
-that PNG uses. Since PDF does not support images with transparency and since
-img2pdf aims to never be lossy, input images with an alpha channel are not
-supported.
+that PNG uses.
+For images with transparency, the alpha channel will be stored as a separate
+soft mask. This is lossless, too.
The output is sent to standard output so that it can be redirected into a file
or to another program as part of a shell pipe. To directly write the output
@@ -2203,7 +3895,9 @@ Paper sizes:
the value in the second column has the same effect as giving the short hand
in the first column. Appending ^T (a caret/circumflex followed by the letter
T) turns the paper size from portrait into landscape. The postfix thus
- symbolizes the transpose. The values are case insensitive.
+ symbolizes the transpose. Note that on Windows cmd.exe the caret symbol is
+ the escape character, so you need to put quotes around the option value.
+ The values are case insensitive.
%s
@@ -2270,7 +3964,7 @@ Examples:
while preserving its aspect ratio and a print border of 2 cm on the top and
bottom and 2.5 cm on the left and right hand side.
- $ img2pdf --output out.pdf --pagesize A4^T --border 2cm:2.5cm *.jpg
+ $ img2pdf --output out.pdf --pagesize "A4^T" --border 2cm:2.5cm *.jpg
On each A4 page, fit images into a 10 cm times 15 cm rectangle but keep the
original image size if the image is smaller than that.
@@ -2288,7 +3982,7 @@ Examples:
$ img2pdf --output out.pdf --colorspace L input.jp2
-Written by Johannes 'josch' Schauer <josch@mister-muffin.de>
+Written by Johannes Schauer Marin Rodrigues <josch@mister-muffin.de>
Report bugs at https://gitlab.mister-muffin.de/josch/img2pdf/issues
"""
@@ -2304,8 +3998,10 @@ Report bugs at https://gitlab.mister-muffin.de/josch/img2pdf/issues
"the Python Imaging Library (PIL). If no input images are given, then "
'a single image is read from standard input. The special filename "-" '
"can be used once to read an image from standard input. To read a "
- 'file in the current directory with the filename "-", pass it to '
- 'img2pdf by explicitly stating its relative path like "./-".',
+ 'file in the current directory with the filename "-" (or with a '
+ 'filename starting with "-"), pass it to img2pdf by explicitly '
+ 'stating its relative path like "./-". Cannot be used together with '
+ "--from-file.",
)
parser.add_argument(
"-v",
@@ -2321,6 +4017,19 @@ Report bugs at https://gitlab.mister-muffin.de/josch/img2pdf/issues
version="%(prog)s " + __version__,
help="Prints version information and exits.",
)
+ parser.add_argument(
+ "--from-file",
+ metavar="FILE",
+ type=from_file,
+ default=[],
+ help="Read the list of images from FILE instead of passing them as "
+ "positional arguments. If this option is used, then the list of "
+ "positional arguments must be empty. The paths to the input images "
+ 'in FILE are separated by NUL bytes. If FILE is "-" then the paths '
+ "are expected on standard input. This option is useful if you want "
+ "to pass more images than the maximum command length of your shell "
+ "permits. This option can be used with commands like `find -print0`.",
+ )
outargs = parser.add_argument_group(
title="General output arguments",
@@ -2364,15 +4073,18 @@ RGB.""",
)
outargs.add_argument(
- "--without-pdfrw",
- action="store_true",
- help="By default, img2pdf uses the pdfrw library to create the output "
- "PDF if pdfrw is available. If you want to use the internal PDF "
- "generator of img2pdf even if pdfrw is present, then pass this "
- "option. This can be useful if you want to have unicode metadata "
- "values which pdfrw does not yet support (See "
- "https://github.com/pmaupin/pdfrw/issues/39) or if you want the "
- "PDF code to be more human readable.",
+ "--engine",
+ metavar="engine",
+ type=parse_enginearg,
+ help="Choose PDF engine. Can be either internal, pikepdf or pdfrw. "
+ "The internal engine does not have additional requirements and writes "
+ "out a human readable PDF. The pikepdf engine requires the pikepdf "
+ "Python module and qpdf library, is most featureful, can "
+ 'linearize PDFs ("fast web view") and can compress more parts of it.'
+ "The pdfrw engine requires the pdfrw Python "
+ "module but does not support unicode metadata (See "
+ "https://github.com/pmaupin/pdfrw/issues/39) or palette data (See "
+ "https://github.com/pmaupin/pdfrw/issues/128).",
)
outargs.add_argument(
@@ -2385,6 +4097,17 @@ RGB.""",
)
outargs.add_argument(
+ "--include-thumbnails",
+ action="store_true",
+ help="Some multi-frame formats like MPO carry a main image and "
+ "one or more scaled-down copies of the main image (thumbnails). "
+ "In such a case, img2pdf will only include the main image and "
+ "not create additional pages for each of the thumbnails. If this "
+ "option is set, img2pdf will instead create one page per frame and "
+ "thus store each thumbnail on its own page.",
+ )
+
+ outargs.add_argument(
"--pillow-limit-break",
action="store_true",
help="img2pdf uses the Python Imaging Library Pillow to read input "
@@ -2395,6 +4118,30 @@ RGB.""",
% Image.MAX_IMAGE_PIXELS,
)
+ if sys.platform == "win32":
+ # on Windows, there are no default paths to search for an ICC profile
+ # so make the argument required instead of optional
+ outargs.add_argument(
+ "--pdfa",
+ type=validate_icc,
+ help="Output a PDF/A-1b compliant document. The argument to this "
+ "option is the path to the ICC profile that will be embedded into "
+ "the resulting PDF.",
+ )
+ else:
+ outargs.add_argument(
+ "--pdfa",
+ nargs="?",
+ const=get_default_icc_profile(),
+ default=None,
+ type=validate_icc,
+ help="Output a PDF/A-1b compliant document. By default, this will "
+ "embed either /usr/share/color/icc/sRGB.icc, "
+ "/usr/share/color/icc/OpenICC/sRGB.icc or "
+ "/usr/share/color/icc/colord/sRGB.icc as the color profile, whichever "
+ "is found to exist first.",
+ )
+
sizeargs = parser.add_argument_group(
title="Image and page size and layout arguments",
description="""\
@@ -2439,6 +4186,8 @@ the image size will be calculated from the page size, respecting the border
setting. If the --border option is given while both the --pagesize and
--imgsize options are passed, then the --border option will be ignored.
+The --pagesize option or the --imgsize option with the --border option will
+determine the MediaBox size of the resulting PDF document.
"""
% default_dpi,
)
@@ -2508,6 +4257,68 @@ of the input image. If the orientation of a page gets flipped, then so do the
values set via the --border option.
""",
)
+ sizeargs.add_argument(
+ "-r",
+ "--rotation",
+ "--orientation",
+ metavar="ROT",
+ type=parse_rotationarg,
+ default=Rotation.auto,
+ help="""
+Specifies how input images should be rotated. ROT can be one of auto, none,
+ifvalid, 0, 90, 180 and 270. The default value is auto and indicates that input
+images are rotated according to their EXIF Orientation tag. The values none and
+0 ignore the EXIF Orientation values of the input images. The value ifvalid
+acts like auto but ignores invalid EXIF rotation values and only issues a
+warning instead of throwing an error. This is useful because many devices like
+Android phones, Canon cameras or scanners emit an invalid Orientation tag value
+of zero. The values 90, 180 and 270 perform a clockwise rotation of the image.
+ """,
+ )
+ sizeargs.add_argument(
+ "--crop-border",
+ metavar="L[:L]",
+ type=parse_borderarg,
+ help="""
+Specifies the border between the CropBox and the MediaBox. One, or two length
+values can be given as an argument, separated by a colon. One value specifies
+the border on all four sides. Two values specify the border on the top/bottom
+and left/right, respectively. It is not possible to specify asymmetric borders.
+""",
+ )
+ sizeargs.add_argument(
+ "--bleed-border",
+ metavar="L[:L]",
+ type=parse_borderarg,
+ help="""
+Specifies the border between the BleedBox and the MediaBox. One, or two length
+values can be given as an argument, separated by a colon. One value specifies
+the border on all four sides. Two values specify the border on the top/bottom
+and left/right, respectively. It is not possible to specify asymmetric borders.
+""",
+ )
+ sizeargs.add_argument(
+ "--trim-border",
+ metavar="L[:L]",
+ type=parse_borderarg,
+ help="""
+Specifies the border between the TrimBox and the MediaBox. One, or two length
+values can be given as an argument, separated by a colon. One value specifies
+the border on all four sides. Two values specify the border on the top/bottom
+and left/right, respectively. It is not possible to specify asymmetric borders.
+""",
+ )
+ sizeargs.add_argument(
+ "--art-border",
+ metavar="L[:L]",
+ type=parse_borderarg,
+ help="""
+Specifies the border between the ArtBox and the MediaBox. One, or two length
+values can be given as an argument, separated by a colon. One value specifies
+the border on all four sides. Two values specify the border on the top/bottom
+and left/right, respectively. It is not possible to specify asymmetric borders.
+""",
+ )
metaargs = parser.add_argument_group(
title="Arguments setting metadata",
@@ -2599,12 +4410,14 @@ values set via the --border option.
'Valid values are "single" (display single pages), "onecolumn" '
'(one continuous column), "twocolumnright" (two continuous '
'columns with odd number pages on the right) and "twocolumnleft" '
- "(two continuous columns with odd numbered pages on the left)",
+ "(two continuous columns with odd numbered pages on the left), "
+ '"twopageright" (two pages with odd numbered page on the right) '
+ 'and "twopageleft" (two pages with odd numbered page on the left)',
)
viewerargs.add_argument(
"--viewer-fit-window",
action="store_true",
- help="Instruct the PDF viewer to resize the window to fit the page " "size",
+ help="Instruct the PDF viewer to resize the window to fit the page size",
)
viewerargs.add_argument(
"--viewer-center-window",
@@ -2616,8 +4429,11 @@ values set via the --border option.
action="store_true",
help="Instruct the PDF viewer to open the PDF in fullscreen mode",
)
+ return parser
+
- args = parser.parse_args(argv[1:])
+def main(argv=sys.argv):
+ args = get_main_parser().parse_args(argv[1:])
if args.verbose:
logging.basicConfig(level=logging.DEBUG)
@@ -2629,39 +4445,53 @@ values set via the --border option.
args.pagesize, args.imgsize, args.border, args.fit, args.auto_orient
)
- # if no positional arguments were supplied, read a single image from
- # standard input
- if len(args.images) == 0:
- logging.info("reading image from standard input")
+ if len(args.images) > 0 and len(args.from_file) > 0:
+ logger.error(
+ "%s: error: cannot use --from-file with positional arguments" % parser.prog
+ )
+ sys.exit(2)
+ elif len(args.images) == 0 and len(args.from_file) == 0:
+ # if no positional arguments were supplied, read a single image from
+ # standard input
+ print(
+ "Reading image from standard input...\n"
+ "Re-run with -h or --help for usage information.",
+ file=sys.stderr,
+ )
try:
- if PY3:
- args.images = [sys.stdin.buffer.read()]
- else:
- args.images = [sys.stdin.read()]
+ images = [sys.stdin.buffer.read()]
except KeyboardInterrupt:
- exit(0)
+ sys.exit(0)
+ elif len(args.images) > 0 and len(args.from_file) == 0:
+ # On windows, each positional argument can expand into multiple paths
+ # because we do globbing ourselves. Here we flatten the list of lists
+ # again.
+ images = list(chain.from_iterable(args.images))
+ elif len(args.images) == 0 and len(args.from_file) > 0:
+ images = args.from_file
# with the number of pages being equal to the number of images, the
# value passed to --viewer-initial-page must be between 1 and that number
if args.viewer_initial_page is not None:
if args.viewer_initial_page < 1:
parser.print_usage(file=sys.stderr)
- logging.error(
+ logger.error(
"%s: error: argument --viewer-initial-page: must be "
"greater than zero" % parser.prog
)
- exit(2)
- if args.viewer_initial_page > len(args.images):
+ sys.exit(2)
+ if args.viewer_initial_page > len(images):
parser.print_usage(file=sys.stderr)
- logging.error(
+ logger.error(
"%s: error: argument --viewer-initial-page: must be "
"less than or equal to the total number of pages" % parser.prog
)
- exit(2)
+ sys.exit(2)
try:
convert(
- *args.images,
+ *images,
+ engine=args.engine,
title=args.title,
author=args.author,
creator=args.creator,
@@ -2680,17 +4510,23 @@ values set via the --border option.
viewer_fit_window=args.viewer_fit_window,
viewer_center_window=args.viewer_center_window,
viewer_fullscreen=args.viewer_fullscreen,
- with_pdfrw=not args.without_pdfrw,
outputstream=args.output,
- first_frame_only=args.first_frame_only
+ first_frame_only=args.first_frame_only,
+ cropborder=args.crop_border,
+ bleedborder=args.bleed_border,
+ trimborder=args.trim_border,
+ artborder=args.art_border,
+ pdfa=args.pdfa,
+ rotation=args.rotation,
+ include_thumbnails=args.include_thumbnails,
)
except Exception as e:
- logging.error("error: " + str(e))
- if logging.getLogger().isEnabledFor(logging.DEBUG):
+ logger.error("error: " + str(e))
+ if logger.isEnabledFor(logging.DEBUG):
import traceback
traceback.print_exc(file=sys.stderr)
- exit(1)
+ sys.exit(1)
if __name__ == "__main__":
diff --git a/src/img2pdf_test.py b/src/img2pdf_test.py
new file mode 100755
index 0000000..4882092
--- /dev/null
+++ b/src/img2pdf_test.py
@@ -0,0 +1,7159 @@
+#!/usr/bin/env python3
+
+import sys
+import numpy
+import scipy.signal
+import zlib
+import struct
+import subprocess
+import pytest
+import re
+import pikepdf
+import hashlib
+import img2pdf
+import os
+from io import BytesIO
+from PIL import Image
+import decimal
+from packaging.version import parse as parse_version
+import warnings
+import json
+import pathlib
+import itertools
+import xml.etree.ElementTree as ET
+
+img2pdfprog = os.getenv("img2pdfprog", default="src/img2pdf.py")
+
+ICC_PROFILE = None
+ICC_PROFILE_PATHS = (
+ # Debian
+ "/usr/share/color/icc/ghostscript/srgb.icc",
+ # Fedora
+ "/usr/share/ghostscript/iccprofiles/srgb.icc",
+ # Archlinux and Gentoo
+ "/usr/share/ghostscript/*/iccprofiles/srgb.icc",
+)
+for glob in ICC_PROFILE_PATHS:
+ for path in pathlib.Path("/").glob(glob.lstrip("/")):
+ if path.is_file():
+ ICC_PROFILE = path
+ break
+
+HAVE_FAKETIME = True
+try:
+ ver = subprocess.check_output(["faketime", "--version"])
+ if b"faketime: Version " not in ver:
+ HAVE_FAKETIME = False
+except FileNotFoundError:
+ HAVE_FAKETIME = False
+
+HAVE_MUTOOL = True
+try:
+ ver = subprocess.check_output(["mutool", "-v"], stderr=subprocess.STDOUT)
+ m = re.fullmatch(r"mutool version ([0-9.]+)\n", ver.decode("utf8"))
+ if m is None:
+ HAVE_MUTOOL = False
+ else:
+ if parse_version(m.group(1)) < parse_version("1.10.0"):
+ HAVE_MUTOOL = False
+except FileNotFoundError:
+ HAVE_MUTOOL = False
+
+if not HAVE_MUTOOL:
+ warnings.warn("mutool >= 1.10.0 not available, skipping checks...")
+
+HAVE_PDFIMAGES_CMYK = True
+try:
+ ver = subprocess.check_output(["pdfimages", "-v"], stderr=subprocess.STDOUT)
+ m = re.fullmatch(r"pdfimages version ([0-9.]+)", ver.split(b"\n")[0].decode("utf8"))
+ if m is None:
+ HAVE_PDFIMAGES_CMYK = False
+ else:
+ if parse_version(m.group(1)) < parse_version("0.42.0"):
+ HAVE_PDFIMAGES_CMYK = False
+except FileNotFoundError:
+ HAVE_PDFIMAGES_CMYK = False
+
+if not HAVE_PDFIMAGES_CMYK:
+ warnings.warn("pdfimages >= 0.42.0 not available, skipping CMYK checks...")
+
+for prog in ["convert", "compare", "identify"]:
+ try:
+ subprocess.check_call([prog] + ["-version"], stderr=subprocess.STDOUT)
+ globals()[prog.upper()] = [prog]
+ except subprocess.CalledProcessError:
+ globals()[prog.upper()] = ["magick", prog]
+
+HAVE_IMAGEMAGICK_MODERN = True
+try:
+ ver = subprocess.check_output(CONVERT + ["-version"], stderr=subprocess.STDOUT)
+ m = re.fullmatch(
+ r"Version: ImageMagick ([0-9.]+-[0-9]+) .*", ver.split(b"\n")[0].decode("utf8")
+ )
+ if m is None:
+ HAVE_IMAGEMAGICK_MODERN = False
+ else:
+ if parse_version(m.group(1)) < parse_version("6.9.10-12"):
+ HAVE_IMAGEMAGICK_MODERN = False
+except FileNotFoundError:
+ HAVE_IMAGEMAGICK_MODERN = False
+except subprocess.CalledProcessError:
+ HAVE_IMAGEMAGICK_MODERN = False
+
+if not HAVE_IMAGEMAGICK_MODERN:
+ warnings.warn("imagemagick >= 6.9.10-12 not available, skipping certain checks...")
+
+HAVE_JP2 = True
+try:
+ ver = subprocess.check_output(
+ IDENTIFY + ["-list", "format"], stderr=subprocess.STDOUT
+ )
+ found = False
+ for line in ver.split(b"\n"):
+ if re.match(rb"\s+JP2\* JP2\s+rw-\s+JPEG-2000 File Format Syntax", line):
+ found = True
+ break
+ if not found:
+ HAVE_JP2 = False
+except FileNotFoundError:
+ HAVE_JP2 = False
+except subprocess.CalledProcessError:
+ HAVE_JP2 = False
+
+if not HAVE_JP2:
+ warnings.warn("imagemagick has no jpeg 2000 support, skipping certain checks...")
+
+# the result of compare -metric PSNR is either just a floating point value or a
+# floating point value following by the same value multiplied by 0.01,
+# surrounded in parenthesis since ImagemMagick 7.1.0-48:
+# https://github.com/ImageMagick/ImageMagick/commit/751829cd4c911d7a42953a47c1f73068d9e7da2f
+psnr_re = re.compile(rb"((?:inf|(?:0|[1-9][0-9]*)(?:\.[0-9]+)?))(?: \([0-9.]+\))?")
+
+###############################################################################
+# HELPER FUNCTIONS #
+###############################################################################
+
+
+# Interpret a datetime string in a given timezone and format it according to a
+# given format string in in UTC.
+# We avoid using the Python datetime module for this job because doing so would
+# just replicate the code we want to test for correctness.
+def tz2utcstrftime(string, fmt, timezone):
+ return (
+ subprocess.check_output(
+ [
+ "date",
+ "--utc",
+ f'--date=TZ="{timezone}" {string}',
+ f"+{fmt}",
+ ]
+ )
+ .decode("utf8")
+ .removesuffix("\n")
+ )
+
+
+def find_closest_palette_color(color, palette):
+ if color.ndim == 0:
+ idx = (numpy.abs(palette - color)).argmin()
+ else:
+ # naive distance function by computing the euclidean distance in RGB space
+ idx = ((palette - color) ** 2).sum(axis=-1).argmin()
+ return palette[idx]
+
+
+def floyd_steinberg(img, palette):
+ result = numpy.array(img, copy=True)
+ for y in range(result.shape[0]):
+ for x in range(result.shape[1]):
+ oldpixel = result[y, x]
+ newpixel = find_closest_palette_color(oldpixel, palette)
+ quant_error = oldpixel - newpixel
+ result[y, x] = newpixel
+ if x + 1 < result.shape[1]:
+ result[y, x + 1] += quant_error * 7 / 16
+ if y + 1 < result.shape[0]:
+ result[y + 1, x - 1] += quant_error * 3 / 16
+ result[y + 1, x] += quant_error * 5 / 16
+ if x + 1 < result.shape[1] and y + 1 < result.shape[0]:
+ result[y + 1, x + 1] += quant_error * 1 / 16
+ return result
+
+
+def convolve_rgba(img, kernel):
+ return numpy.stack(
+ (
+ scipy.signal.convolve2d(img[:, :, 0], kernel, "same"),
+ scipy.signal.convolve2d(img[:, :, 1], kernel, "same"),
+ scipy.signal.convolve2d(img[:, :, 2], kernel, "same"),
+ scipy.signal.convolve2d(img[:, :, 3], kernel, "same"),
+ ),
+ axis=-1,
+ )
+
+
+def rgb2gray(img):
+ result = numpy.zeros((60, 60), dtype=numpy.dtype("int64"))
+ count = 0
+ for y in range(img.shape[0]):
+ for x in range(img.shape[1]):
+ clin = sum(img[y, x] * [0.2126, 0.7152, 0.0722]) / 0xFFFF
+ if clin <= 0.0031308:
+ csrgb = 12.92 * clin
+ else:
+ csrgb = 1.055 * clin ** (1 / 2.4) - 0.055
+ result[y, x] = csrgb * 0xFFFF
+ count += 1
+ # if count == 24:
+ # raise Exception(result[y, x])
+ return result
+
+
+def palettize(img, pal):
+ result = numpy.zeros((img.shape[0], img.shape[1]), dtype=numpy.dtype("int64"))
+ for y in range(img.shape[0]):
+ for x in range(img.shape[1]):
+ for i, col in enumerate(pal):
+ if numpy.array_equal(img[y, x], col):
+ result[y, x] = i
+ break
+ else:
+ raise Exception()
+ return result
+
+
+# we cannot use zlib.compress() because different compressors may compress the
+# same data differently, for example by using different optimizations on
+# different architectures:
+# https://lists.fedoraproject.org/archives/list/devel@lists.fedoraproject.org/thread/R7GD4L5Z6HELCDAL2RDESWR2F3ZXHWVX/
+#
+# to make the compressed representation of the uncompressed data bit-by-bit
+# identical on all platforms we make use of the compression method 0, that is,
+# no compression at all :)
+def compress(data):
+ # two-byte zlib header (rfc1950)
+ # common header for lowest compression level
+ # bits 0-3: Compression info, base-2 logarithm of the LZ77 window size,
+ # minus eight -- 7 indicates a 32K window size
+ # bits 4-7: Compression method -- 8 is deflate
+ # bits 8-9: Compression level -- 0 is fastest
+ # bit 10: preset dictionary -- 0 is none
+ # bits 11-15: check bits so that the 16-bit unsigned integer stored in MSB
+ # order is a multiple of 31
+ result = b"\x78\x01"
+ # content is stored in deflate format (rfc1951)
+ # maximum chunk size is the largest 16 bit unsigned integer
+ chunksize = 0xFFFF
+ for i in range(0, len(data), chunksize):
+ # bits 0-4 are unused
+ # bits 5-6 indicate compression method -- 0 is no compression
+ # bit 7 indicates the last chunk
+ if i * chunksize < len(data) - chunksize:
+ result += b"\x00"
+ else:
+ # last chunck
+ result += b"\x01"
+ chunk = data[i : i + chunksize]
+ # the chunk length as little endian 16 bit unsigned integer
+ result += struct.pack("<H", len(chunk))
+ # the one's complement of the chunk length
+ # one's complement is all bits inverted which is the result of
+ # xor with 0xffff for a 16 bit unsigned integer
+ result += struct.pack("<H", len(chunk) ^ 0xFFFF)
+ result += chunk
+ # adler32 checksum of the uncompressed data as big endian 32 bit unsigned
+ # integer
+ result += struct.pack(">I", zlib.adler32(data))
+ return result
+
+
+def write_png(data, path, bitdepth, colortype, palette=None, iccp=None):
+ with open(str(path), "wb") as f:
+ f.write(b"\x89PNG\r\n\x1A\n")
+ # PNG image type Colour type Allowed bit depths
+ # Greyscale 0 1, 2, 4, 8, 16
+ # Truecolour 2 8, 16
+ # Indexed-colour 3 1, 2, 4, 8
+ # Greyscale with alpha 4 8, 16
+ # Truecolour with alpha 6 8, 16
+ block = b"IHDR" + struct.pack(
+ ">IIBBBBB",
+ data.shape[1], # width
+ data.shape[0], # height
+ bitdepth, # bitdepth
+ colortype, # colortype
+ 0, # compression
+ 0, # filtertype
+ 0, # interlaced
+ )
+ f.write(
+ struct.pack(">I", len(block) - 4)
+ + block
+ + struct.pack(">I", zlib.crc32(block))
+ )
+ if iccp is not None:
+ with open(iccp, "rb") as infh:
+ iccdata = infh.read()
+ block = b"iCCP"
+ block += b"icc\0" # arbitrary profile name
+ block += b"\0" # compression method (deflate)
+ block += zlib.compress(iccdata)
+ f.write(
+ struct.pack(">I", len(block) - 4)
+ + block
+ + struct.pack(">I", zlib.crc32(block))
+ )
+ if palette is not None:
+ block = b"PLTE"
+ for col in palette:
+ block += struct.pack(">BBB", col[0], col[1], col[2])
+ f.write(
+ struct.pack(">I", len(block) - 4)
+ + block
+ + struct.pack(">I", zlib.crc32(block))
+ )
+ raw = b""
+ for y in range(data.shape[0]):
+ raw += b"\0"
+ if bitdepth == 16:
+ raw += data[y].astype(">u2").tobytes()
+ elif bitdepth == 8:
+ raw += data[y].astype(">u1").tobytes()
+ elif bitdepth in [4, 2, 1]:
+ valsperbyte = 8 // bitdepth
+ for x in range(0, data.shape[1], valsperbyte):
+ val = 0
+ for j in range(valsperbyte):
+ if x + j >= data.shape[1]:
+ break
+ val |= (data[y, x + j].astype(">u2") & (2**bitdepth - 1)) << (
+ (valsperbyte - j - 1) * bitdepth
+ )
+ raw += struct.pack(">B", val)
+ else:
+ raise Exception()
+ compressed = compress(raw)
+ block = b"IDAT" + compressed
+ f.write(
+ struct.pack(">I", len(compressed))
+ + block
+ + struct.pack(">I", zlib.crc32(block))
+ )
+ block = b"IEND"
+ f.write(struct.pack(">I", 0) + block + struct.pack(">I", zlib.crc32(block)))
+
+
+def compare(im1, im2, exact, icc, cmyk):
+ if exact:
+ if icc:
+ raise Exception("icc cannot be exact")
+ else:
+ subprocess.check_call(
+ COMPARE
+ + [
+ "-metric",
+ "AE",
+ "-alpha",
+ "off",
+ im1,
+ im2,
+ "null:",
+ ]
+ )
+ else:
+ iccargs = []
+ if icc:
+ if ICC_PROFILE is None:
+ pytest.skip("Could not locate an ICC profile")
+ iccargs = ["-profile", ICC_PROFILE]
+ psnr = subprocess.run(
+ COMPARE
+ + iccargs
+ + [
+ "-metric",
+ "PSNR",
+ im1,
+ im2,
+ "null:",
+ ],
+ check=False,
+ stderr=subprocess.PIPE,
+ ).stderr
+ assert psnr != b"0"
+ assert psnr != b"0 (0)"
+ assert psnr_re.fullmatch(psnr) is not None, psnr
+ psnr = psnr_re.fullmatch(psnr).group(1)
+ psnr = float(psnr)
+ assert psnr != 0 # or otherwise we would use the exact variant
+ assert psnr > 50
+
+
+def compare_ghostscript(tmpdir, img, pdf, gsdevice="png16m", exact=True, icc=False):
+ if gsdevice in ["png16m", "pnggray"]:
+ ext = "png"
+ elif gsdevice in ["tiff24nc", "tiff32nc", "tiff48nc"]:
+ ext = "tiff"
+ else:
+ raise Exception("unknown gsdevice: " + gsdevice)
+ subprocess.check_call(
+ [
+ "gs",
+ "-dQUIET",
+ "-dNOPAUSE",
+ "-dBATCH",
+ "-sDEVICE=" + gsdevice,
+ "-r96",
+ "-sOutputFile=" + str(tmpdir / "gs-") + "%00d." + ext,
+ str(pdf),
+ ]
+ )
+ compare(str(img), str(tmpdir / "gs-1.") + ext, exact, icc, False)
+ (tmpdir / ("gs-1." + ext)).unlink()
+
+
+def compare_poppler(tmpdir, img, pdf, exact=True, icc=False):
+ subprocess.check_call(
+ ["pdftocairo", "-r", "96", "-png", str(pdf), str(tmpdir / "poppler")]
+ )
+ compare(str(img), str(tmpdir / "poppler-1.png"), exact, icc, False)
+ (tmpdir / "poppler-1.png").unlink()
+
+
+def compare_mupdf(tmpdir, img, pdf, exact=True, cmyk=False):
+ if not HAVE_MUTOOL:
+ return
+ if cmyk:
+ out = tmpdir / "mupdf.pam"
+ subprocess.check_call(
+ ["mutool", "draw", "-r", "96", "-c", "cmyk", "-o", str(out), str(pdf)]
+ )
+ else:
+ out = tmpdir / "mupdf.png"
+ subprocess.check_call(
+ ["mutool", "draw", "-r", "96", "-png", "-o", str(out), str(pdf)]
+ )
+ compare(str(img), str(out), exact, False, cmyk)
+ out.unlink()
+
+
+def compare_pdfimages_jpg(tmpdir, img, pdf):
+ subprocess.check_call(["pdfimages", "-j", str(pdf), str(tmpdir / "images")])
+ assert img.read_bytes() == (tmpdir / "images-000.jpg").read_bytes()
+ (tmpdir / "images-000.jpg").unlink()
+
+
+def compare_pdfimages_cmyk(tmpdir, img, pdf):
+ if not HAVE_PDFIMAGES_CMYK:
+ return
+ subprocess.check_call(["pdfimages", "-j", str(pdf), str(tmpdir / "images")])
+ assert img.read_bytes() == (tmpdir / "images-000.jpg").read_bytes()
+ (tmpdir / "images-000.jpg").unlink()
+
+
+def compare_pdfimages_jp2(tmpdir, img, pdf):
+ subprocess.check_call(["pdfimages", "-jp2", str(pdf), str(tmpdir / "images")])
+ assert img.read_bytes() == (tmpdir / "images-000.jp2").read_bytes()
+ (tmpdir / "images-000.jp2").unlink()
+
+
+def compare_pdfimages_tiff(tmpdir, img, pdf):
+ subprocess.check_call(["pdfimages", "-tiff", str(pdf), str(tmpdir / "images")])
+ subprocess.check_call(
+ COMPARE
+ + [
+ "-metric",
+ "AE",
+ str(img),
+ str(tmpdir / "images-000.tif"),
+ "null:",
+ ]
+ )
+ (tmpdir / "images-000.tif").unlink()
+
+
+def compare_pdfimages_png(tmpdir, img, pdf, exact=True, icc=False):
+ subprocess.check_call(["pdfimages", "-png", str(pdf), str(tmpdir / "images")])
+ # images-001.png is the grayscale SMask image (the original alpha channel)
+ if os.path.isfile(tmpdir / "images-001.png"):
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmpdir / "images-000.png"),
+ str(tmpdir / "images-001.png"),
+ "-compose",
+ "copy-opacity",
+ "-composite",
+ str(tmpdir / "composite.png"),
+ ]
+ )
+ (tmpdir / "images-000.png").unlink()
+ (tmpdir / "images-001.png").unlink()
+ os.rename(tmpdir / "composite.png", tmpdir / "images-000.png")
+
+ if exact:
+ if icc:
+ raise Exception("not exact with icc")
+ subprocess.check_call(
+ COMPARE
+ + [
+ "-metric",
+ "AE",
+ str(img),
+ str(tmpdir / "images-000.png"),
+ "null:",
+ ]
+ )
+ else:
+ if icc:
+ if ICC_PROFILE is None:
+ pytest.skip("Could not locate an ICC profile")
+ psnr = subprocess.run(
+ COMPARE
+ + [
+ "-metric",
+ "PSNR",
+ "(",
+ "-profile",
+ ICC_PROFILE,
+ "-depth",
+ "8",
+ str(img),
+ ")",
+ str(tmpdir / "images-000.png"),
+ "null:",
+ ],
+ check=False,
+ stderr=subprocess.PIPE,
+ ).stderr
+ else:
+ psnr = subprocess.run(
+ COMPARE
+ + [
+ "-metric",
+ "PSNR",
+ str(img),
+ str(tmpdir / "images-000.png"),
+ "null:",
+ ],
+ check=False,
+ stderr=subprocess.PIPE,
+ ).stderr
+ assert psnr != b"0"
+ assert psnr != b"0 (0)"
+ psnr = psnr_re.fullmatch(psnr).group(1)
+ psnr = float(psnr)
+ assert psnr != 0 # or otherwise we would use the exact variant
+ assert psnr > 50
+ (tmpdir / "images-000.png").unlink()
+
+
+def tiff_header_for_ccitt(width, height, img_size, ccitt_group=4):
+ # Quick and dirty TIFF header builder from
+ # https://stackoverflow.com/questions/2641770
+ tiff_header_struct = "<" + "2s" + "h" + "l" + "h" + "hhll" * 8 + "h"
+ return struct.pack(
+ # fmt: off
+ tiff_header_struct,
+ b'II', # Byte order indication: Little indian
+ 42, # Version number (always 42)
+ 8, # Offset to first IFD
+ 8, # Number of tags in IFD
+ 256, 4, 1, width, # ImageWidth, LONG, 1, width
+ 257, 4, 1, height, # ImageLength, LONG, 1, lenght
+ 258, 3, 1, 1, # BitsPerSample, SHORT, 1, 1
+ 259, 3, 1, ccitt_group, # Compression, SHORT, 1, 4 = CCITT Group 4
+ 262, 3, 1, 1, # Threshholding, SHORT, 1, 0 = WhiteIsZero
+ 273, 4, 1, struct.calcsize(
+ tiff_header_struct), # StripOffsets, LONG, 1, len of header
+ 278, 4, 1, height, # RowsPerStrip, LONG, 1, lenght
+ 279, 4, 1, img_size, # StripByteCounts, LONG, 1, size of image
+ 0
+ # last IFD
+ # fmt: on
+ )
+
+
+pixel_R = [
+ [1, 1, 1, 0],
+ [1, 0, 0, 1],
+ [1, 0, 0, 1],
+ [1, 1, 1, 0],
+ [1, 0, 0, 1],
+ [1, 0, 0, 1],
+ [1, 0, 0, 1],
+]
+pixel_G = [
+ [0, 1, 1, 0],
+ [1, 0, 0, 1],
+ [1, 0, 0, 0],
+ [1, 0, 1, 1],
+ [1, 0, 0, 1],
+ [1, 0, 0, 1],
+ [0, 1, 1, 0],
+]
+pixel_B = [
+ [1, 1, 1, 0],
+ [1, 0, 0, 1],
+ [1, 0, 0, 1],
+ [1, 1, 1, 0],
+ [1, 0, 0, 1],
+ [1, 0, 0, 1],
+ [1, 1, 1, 0],
+]
+
+
+def alpha_value():
+ # gaussian kernel with sigma=3
+ kernel = numpy.array(
+ [
+ [0.011362, 0.014962, 0.017649, 0.018648, 0.017649, 0.014962, 0.011362],
+ [0.014962, 0.019703, 0.02324, 0.024556, 0.02324, 0.019703, 0.014962],
+ [0.017649, 0.02324, 0.027413, 0.028964, 0.027413, 0.02324, 0.017649],
+ [0.018648, 0.024556, 0.028964, 0.030603, 0.028964, 0.024556, 0.018648],
+ [0.017649, 0.02324, 0.027413, 0.028964, 0.027413, 0.02324, 0.017649],
+ [0.014962, 0.019703, 0.02324, 0.024556, 0.02324, 0.019703, 0.014962],
+ [0.011362, 0.014962, 0.017649, 0.018648, 0.017649, 0.014962, 0.011362],
+ ],
+ float,
+ )
+
+ # constructs a 2D array of a circle with a width of 36
+ circle = list()
+ offsets_36 = [14, 11, 9, 7, 6, 5, 4, 3, 3, 2, 2, 1, 1, 1, 0, 0, 0, 0]
+ for offs in offsets_36 + offsets_36[::-1]:
+ circle.append([0] * offs + [1] * (len(offsets_36) - offs) * 2 + [0] * offs)
+
+ alpha = numpy.zeros((60, 60, 4), dtype=numpy.dtype("int64"))
+
+ # draw three circles
+ for xpos, ypos, color in [
+ (12, 3, [0xFFFF, 0, 0, 0xFFFF]),
+ (21, 21, [0, 0xFFFF, 0, 0xFFFF]),
+ (3, 21, [0, 0, 0xFFFF, 0xFFFF]),
+ ]:
+ for x, row in enumerate(circle):
+ for y, pos in enumerate(row):
+ if pos:
+ alpha[y + ypos, x + xpos] += color
+ alpha = numpy.clip(alpha, 0, 0xFFFF)
+ alpha = convolve_rgba(alpha, kernel)
+
+ # draw letters
+ for y, row in enumerate(pixel_R):
+ for x, pos in enumerate(row):
+ if pos:
+ alpha[13 + y, 28 + x] = [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF]
+ for y, row in enumerate(pixel_G):
+ for x, pos in enumerate(row):
+ if pos:
+ alpha[39 + y, 40 + x] = [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF]
+ for y, row in enumerate(pixel_B):
+ for x, pos in enumerate(row):
+ if pos:
+ alpha[39 + y, 15 + x] = [0xFFFF, 0xFFFF, 0xFFFF, 0xFFFF]
+ return alpha
+
+
+def icc_profile():
+ PCS = (0.96420288, 1.0, 0.82490540) # D50 illuminant constants
+ # approximate X,Y,Z values for white, red, green and blue
+ white = (0.95, 1.0, 1.09)
+ red = (0.44, 0.22, 0.014)
+ green = (0.39, 0.72, 0.1)
+ blue = (0.14, 0.06, 0.71)
+
+ getxyz = lambda v: (round(65536 * v[0]), round(65536 * v[1]), round(65536 * v[2]))
+
+ header = (
+ # header
+ +4 * b"\0" # cmmsignatures
+ + 4 * b"\0" # version
+ + b"mntr" # device class
+ + b"RGB " # color space
+ + b"XYZ " # PCS
+ + 12 * b"\0" # datetime
+ + b"\x61\x63\x73\x70" # static signature
+ + 4 * b"\0" # platform
+ + 4 * b"\0" # flags
+ + 4 * b"\0" # device manufacturer
+ + 4 * b"\0" # device model
+ + 8 * b"\0" # device attributes
+ + 4 * b"\0" # rendering intents
+ + struct.pack(">III", *getxyz(PCS))
+ + 4 * b"\0" # creator
+ + 16 * b"\0" # identifier
+ + 28 * b"\0" # reserved
+ )
+
+ def pad4(s):
+ if len(s) % 4 == 0:
+ return s
+ else:
+ return s + b"\x00" * (4 - len(s) % 4)
+
+ tagdata = [
+ b"desc\x00\x00\x00\x00" + struct.pack(">I", 5) + b"fake" + 79 * b"\x00",
+ b"XYZ \x00\x00\x00\x00" + struct.pack(">III", *getxyz(white)),
+ # by mixing up red, green and blue, we create a test profile
+ b"XYZ \x00\x00\x00\x00" + struct.pack(">III", *getxyz(blue)), # red
+ b"XYZ \x00\x00\x00\x00" + struct.pack(">III", *getxyz(red)), # green
+ b"XYZ \x00\x00\x00\x00" + struct.pack(">III", *getxyz(green)), # blue
+ # by only supplying two values, we create the most trivial "curve",
+ # where the remaining values will be linearly interpolated between them
+ b"curv\x00\x00\x00\x00" + struct.pack(">IHH", 2, 0, 65535),
+ b"text\x00\x00\x00\x00" + b"no copyright, use freely" + 1 * b"\x00",
+ ]
+
+ table = [
+ (b"desc", 0),
+ (b"wtpt", 1),
+ (b"rXYZ", 2),
+ (b"gXYZ", 3),
+ (b"bXYZ", 4),
+ # we use the same curve for all three channels, so the same offset is referenced
+ (b"rTRC", 5),
+ (b"gTRC", 5),
+ (b"bTRC", 5),
+ (b"cprt", 6),
+ ]
+
+ offset = (
+ lambda n: 4 # total size
+ + len(header) # header length
+ + 4 # number table entries
+ + len(table) * 12 # table length
+ + sum([len(pad4(s)) for s in tagdata[:n]])
+ )
+
+ table = struct.pack(">I", len(table)) + b"".join(
+ [t + struct.pack(">II", offset(o), len(tagdata[o])) for t, o in table]
+ )
+
+ data = b"".join([pad4(s) for s in tagdata])
+
+ data = (
+ struct.pack(">I", 4 + len(header) + len(table) + len(data))
+ + header
+ + table
+ + data
+ )
+
+ return data
+
+
+###############################################################################
+# INPUT FIXTURES #
+###############################################################################
+
+
+@pytest.fixture(scope="session")
+def alpha():
+ return alpha_value()
+
+
+@pytest.fixture(scope="session")
+def tmp_alpha_png(tmp_path_factory, alpha):
+ tmp_alpha_png = tmp_path_factory.mktemp("alpha_png") / "alpha.png"
+ write_png(alpha, str(tmp_alpha_png), 16, 6)
+ assert (
+ hashlib.md5(tmp_alpha_png.read_bytes()).hexdigest()
+ == "600bb4cffb039a022cec6ed55537deba"
+ )
+ yield tmp_alpha_png
+ tmp_alpha_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_gray1_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ gray16 = rgb2gray(normal16)
+ tmp_gray1_png = tmp_path_factory.mktemp("gray1_png") / "gray1.png"
+ write_png(
+ floyd_steinberg(gray16, numpy.arange(2) / 0x1 * 0xFFFF) / 0xFFFF * 0x1,
+ str(tmp_gray1_png),
+ 1,
+ 0,
+ )
+ assert (
+ hashlib.md5(tmp_gray1_png.read_bytes()).hexdigest()
+ == "dd2c528152d34324747355b73495a115"
+ )
+ yield tmp_gray1_png
+ tmp_gray1_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_gray2_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ gray16 = rgb2gray(normal16)
+ tmp_gray2_png = tmp_path_factory.mktemp("gray2_png") / "gray2.png"
+ write_png(
+ floyd_steinberg(gray16, numpy.arange(4) / 0x3 * 0xFFFF) / 0xFFFF * 0x3,
+ str(tmp_gray2_png),
+ 2,
+ 0,
+ )
+ assert (
+ hashlib.md5(tmp_gray2_png.read_bytes()).hexdigest()
+ == "68e614f4e6a85053d47098dad0ca3976"
+ )
+ yield tmp_gray2_png
+ tmp_gray2_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_gray4_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ gray16 = rgb2gray(normal16)
+ tmp_gray4_png = tmp_path_factory.mktemp("gray4_png") / "gray4.png"
+ write_png(
+ floyd_steinberg(gray16, numpy.arange(16) / 0xF * 0xFFFF) / 0xFFFF * 0xF,
+ str(tmp_gray4_png),
+ 4,
+ 0,
+ )
+ assert (
+ hashlib.md5(tmp_gray4_png.read_bytes()).hexdigest()
+ == "ff04a6fea88133eb77bbb748692ae0fd"
+ )
+ yield tmp_gray4_png
+ tmp_gray4_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_gray8_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ gray16 = rgb2gray(normal16)
+ tmp_gray8_png = tmp_path_factory.mktemp("gray8_png") / "gray8.png"
+ write_png(gray16 / 0xFFFF * 0xFF, tmp_gray8_png, 8, 0)
+ assert (
+ hashlib.md5(tmp_gray8_png.read_bytes()).hexdigest()
+ == "90b4ed9123f295dda7fde499744dede7"
+ )
+ yield tmp_gray8_png
+ tmp_gray8_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_gray16_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ gray16 = rgb2gray(normal16)
+ tmp_gray16_png = tmp_path_factory.mktemp("gray16_png") / "gray16.png"
+ write_png(gray16, str(tmp_gray16_png), 16, 0)
+ assert (
+ hashlib.md5(tmp_gray16_png.read_bytes()).hexdigest()
+ == "f76153d2e72fada11d934c32c8168a57"
+ )
+ yield tmp_gray16_png
+ tmp_gray16_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_inverse_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ tmp_inverse_png = tmp_path_factory.mktemp("inverse_png") / "inverse.png"
+ write_png(0xFF - normal16 / 0xFFFF * 0xFF, str(tmp_inverse_png), 8, 2)
+ assert (
+ hashlib.md5(tmp_inverse_png.read_bytes()).hexdigest()
+ == "0a7d57dc09c4d8fd1ad3511b116c7dfa"
+ )
+ yield tmp_inverse_png
+ tmp_inverse_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_icc_profile(tmp_path_factory):
+ tmp_icc_profile = tmp_path_factory.mktemp("icc_profile") / "fake.icc"
+ tmp_icc_profile.write_bytes(icc_profile())
+ yield tmp_icc_profile
+ tmp_icc_profile.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_icc_png(tmp_path_factory, alpha, tmp_icc_profile):
+ normal16 = alpha[:, :, 0:3]
+ tmp_icc_png = tmp_path_factory.mktemp("icc_png") / "icc.png"
+ write_png(
+ normal16 / 0xFFFF * 0xFF,
+ str(tmp_icc_png),
+ 8,
+ 2,
+ iccp=str(tmp_icc_profile),
+ )
+ assert (
+ hashlib.md5(tmp_icc_png.read_bytes()).hexdigest()
+ == "bf25f673c1617f5f9353b2a043747655"
+ )
+ yield tmp_icc_png
+ tmp_icc_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_normal16_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ tmp_normal16_png = tmp_path_factory.mktemp("normal16_png") / "normal16.png"
+ write_png(normal16, str(tmp_normal16_png), 16, 2)
+ assert (
+ hashlib.md5(tmp_normal16_png.read_bytes()).hexdigest()
+ == "820dd30a2566775fc64c110e8ac65c7e"
+ )
+ yield tmp_normal16_png
+ tmp_normal16_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_normal_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ tmp_normal_png = tmp_path_factory.mktemp("normal_png") / "normal.png"
+ write_png(normal16 / 0xFFFF * 0xFF, str(tmp_normal_png), 8, 2)
+ assert (
+ hashlib.md5(tmp_normal_png.read_bytes()).hexdigest()
+ == "bc30c705f455991cd04be1c298063002"
+ )
+ yield tmp_normal_png
+ tmp_normal_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_palette1_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ tmp_palette1_png = tmp_path_factory.mktemp("palette1_png") / "palette1.png"
+ # don't choose black and white or otherwise imagemagick will classify the
+ # image as bilevel with 8/1-bit depth instead of palette with 8-bit color
+ # don't choose gray colors or otherwise imagemagick will classify the
+ # image as grayscale
+ pal1 = numpy.array(
+ [[0x01, 0x02, 0x03], [0xFE, 0xFD, 0xFC]], dtype=numpy.dtype("int64")
+ )
+ write_png(
+ palettize(
+ floyd_steinberg(normal16, pal1 * 0xFFFF / 0xFF) / 0xFFFF * 0xFF, pal1
+ ),
+ str(tmp_palette1_png),
+ 1,
+ 3,
+ pal1,
+ )
+ assert (
+ hashlib.md5(tmp_palette1_png.read_bytes()).hexdigest()
+ == "3d065f731540e928fb730b3233e4e8a7"
+ )
+ yield tmp_palette1_png
+ tmp_palette1_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_palette2_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ tmp_palette2_png = tmp_path_factory.mktemp("palette2_png") / "palette2.png"
+ # choose values slightly off red, lime and blue because otherwise
+ # imagemagick will classify the image as Depth: 8/1-bit
+ pal2 = numpy.array(
+ [[0, 0, 0], [0xFE, 0, 0], [0, 0xFE, 0], [0, 0, 0xFE]],
+ dtype=numpy.dtype("int64"),
+ )
+ write_png(
+ palettize(
+ floyd_steinberg(normal16, pal2 * 0xFFFF / 0xFF) / 0xFFFF * 0xFF, pal2
+ ),
+ str(tmp_palette2_png),
+ 2,
+ 3,
+ pal2,
+ )
+ assert (
+ hashlib.md5(tmp_palette2_png.read_bytes()).hexdigest()
+ == "0b0d4412c28da26163a622d218ee02ca"
+ )
+ yield tmp_palette2_png
+ tmp_palette2_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_palette4_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ tmp_palette4_png = tmp_path_factory.mktemp("palette4_png") / "palette4.png"
+ # windows 16 color palette
+ pal4 = numpy.array(
+ [
+ [0x00, 0x00, 0x00],
+ [0x80, 0x00, 0x00],
+ [0x00, 0x80, 0x00],
+ [0x80, 0x80, 0x00],
+ [0x00, 0x00, 0x80],
+ [0x80, 0x00, 0x80],
+ [0x00, 0x80, 0x80],
+ [0xC0, 0xC0, 0xC0],
+ [0x80, 0x80, 0x80],
+ [0xFF, 0x00, 0x00],
+ [0x00, 0xFF, 0x00],
+ [0xFF, 0x00, 0x00],
+ [0x00, 0xFF, 0x00],
+ [0xFF, 0x00, 0xFF],
+ [0x00, 0xFF, 0x00],
+ [0xFF, 0xFF, 0xFF],
+ ],
+ dtype=numpy.dtype("int64"),
+ )
+ write_png(
+ palettize(
+ floyd_steinberg(normal16, pal4 * 0xFFFF / 0xFF) / 0xFFFF * 0xFF, pal4
+ ),
+ str(tmp_palette4_png),
+ 4,
+ 3,
+ pal4,
+ )
+ assert (
+ hashlib.md5(tmp_palette4_png.read_bytes()).hexdigest()
+ == "163f6d7964b80eefa0dc6a48cb7315dd"
+ )
+ yield tmp_palette4_png
+ tmp_palette4_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def tmp_palette8_png(tmp_path_factory, alpha):
+ normal16 = alpha[:, :, 0:3]
+ tmp_palette8_png = tmp_path_factory.mktemp("palette8_png") / "palette8.png"
+ # create a 256 color palette by first writing 16 shades of gray
+ # and then writing an array of RGB colors with 6, 8 and 5 levels
+ # for red, green and blue, respectively
+ pal8 = numpy.zeros((256, 3), dtype=numpy.dtype("int64"))
+ i = 0
+ for gray in range(15, 255, 15):
+ pal8[i] = [gray, gray, gray]
+ i += 1
+ for red in 0, 0x33, 0x66, 0x99, 0xCC, 0xFF:
+ for green in 0, 0x24, 0x49, 0x6D, 0x92, 0xB6, 0xDB, 0xFF:
+ for blue in 0, 0x40, 0x80, 0xBF, 0xFF:
+ pal8[i] = [red, green, blue]
+ i += 1
+ assert i == 256
+ write_png(
+ palettize(
+ floyd_steinberg(normal16, pal8 * 0xFFFF / 0xFF) / 0xFFFF * 0xFF, pal8
+ ),
+ str(tmp_palette8_png),
+ 8,
+ 3,
+ pal8,
+ )
+ assert (
+ hashlib.md5(tmp_palette8_png.read_bytes()).hexdigest()
+ == "8847bb734eba0e2d85e3f97fc2849dd4"
+ )
+ yield tmp_palette8_png
+ tmp_palette8_png.unlink()
+
+
+@pytest.fixture(scope="session")
+def jpg_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("jpg") / "in.jpg"
+ subprocess.check_call(CONVERT + [str(tmp_normal_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "JPEG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/jpeg", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert "resolution" not in identify[0]["image"]
+ assert identify[0]["image"].get("units") == "Undefined", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ endian = "endianess" if identify[0].get("version", "0") < "1.0" else "endianness"
+ assert identify[0]["image"].get(endian) == "Undefined", str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "JPEG", str(identify)
+ assert identify[0]["image"].get("orientation") == "Undefined", str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("jpeg:colorspace") == "2"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def jpg_rot_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("jpg_rot") / "in.jpg"
+ subprocess.check_call(CONVERT + [str(tmp_normal_png), str(in_img)])
+ subprocess.check_call(
+ ["exiftool", "-overwrite_original", "-all=", str(in_img), "-n"]
+ )
+ subprocess.check_call(
+ [
+ "exiftool",
+ "-overwrite_original",
+ "-Orientation=6",
+ "-XResolution=96",
+ "-YResolution=96",
+ "-n",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "JPEG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/jpeg", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("resolution") == {"x": 96, "y": 96}
+ assert identify[0]["image"].get("units") == "PixelsPerInch", str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "JPEG", str(identify)
+ assert identify[0]["image"].get("orientation") == "RightTop", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def jpg_cmyk_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("jpg_cmyk") / "in.jpg"
+ subprocess.check_call(
+ CONVERT + [str(tmp_normal_png), "-colorspace", "cmyk", str(in_img)]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "JPEG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/jpeg", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "CMYK", str(identify)
+ assert identify[0]["image"].get("type") == "ColorSeparation", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "JPEG", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def jpg_2000_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("jpg_2000") / "in.jp2"
+ subprocess.check_call(CONVERT + [str(tmp_normal_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "JP2", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/jp2", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "JPEG2000", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def jpg_2000_rgba8_img(tmp_path_factory, tmp_alpha_png):
+ in_img = tmp_path_factory.mktemp("jpg_2000_rgba8") / "in.jp2"
+ subprocess.check_call(CONVERT + [str(tmp_alpha_png), "-depth", "8", str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "JP2", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/jp2", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColorAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "JPEG2000", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def jpg_2000_rgba16_img(tmp_path_factory, tmp_alpha_png):
+ in_img = tmp_path_factory.mktemp("jpg_2000_rgba16") / "in.jp2"
+ subprocess.check_call(CONVERT + [str(tmp_alpha_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "JP2", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/jp2", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColorAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "JPEG2000", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def png_rgb8_img(tmp_normal_png):
+ in_img = tmp_normal_png
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "2"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "2 (Truecolor)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return in_img
+
+
+@pytest.fixture(scope="session")
+def png_rgb16_img(tmp_normal16_png):
+ in_img = tmp_normal16_png
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig")
+ == "16"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "16"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "2"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "2 (Truecolor)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return in_img
+
+
+@pytest.fixture(scope="session")
+def png_rgba8_img(tmp_path_factory, tmp_alpha_png):
+ in_img = tmp_path_factory.mktemp("png_rgba8") / "in.png"
+ subprocess.check_call(
+ CONVERT + [str(tmp_alpha_png), "-depth", "8", "-strip", str(in_img)]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColorAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "6"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "6 (RGBA)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def png_rgba16_img(tmp_alpha_png):
+ in_img = tmp_alpha_png
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColorAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig")
+ == "16"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "16"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "6"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "6 (RGBA)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return in_img
+
+
+@pytest.fixture(scope="session")
+def png_gray8a_img(tmp_path_factory, tmp_alpha_png):
+ in_img = tmp_path_factory.mktemp("png_gray8a") / "in.png"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_alpha_png),
+ "-colorspace",
+ "Gray",
+ "-dither",
+ "FloydSteinberg",
+ "-colors",
+ "256",
+ "-depth",
+ "8",
+ "-strip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "GrayscaleAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "4"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "4 (GrayAlpha)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def png_gray16a_img(tmp_path_factory, tmp_alpha_png):
+ in_img = tmp_path_factory.mktemp("png_gray16a") / "in.png"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_alpha_png),
+ "-colorspace",
+ "Gray",
+ "-depth",
+ "16",
+ "-strip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "GrayscaleAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig")
+ == "16"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "16"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "4"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "4 (GrayAlpha)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def png_interlaced_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("png_interlaced") / "in.png"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal_png),
+ "-interlace",
+ "PNG",
+ "-strip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "2"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "2 (Truecolor)"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.interlace_method")
+ == "1 (Adam7 method)"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def png_gray1_img(tmp_path_factory, tmp_gray1_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_gray1_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") in ["Bilevel", "Grayscale"], str(identify)
+ assert identify[0]["image"].get("depth") == 1, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "1"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "1"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "0"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "0 (Grayscale)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_gray1_png
+
+
+@pytest.fixture(scope="session")
+def png_gray2_img(tmp_path_factory, tmp_gray2_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_gray2_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Grayscale", str(identify)
+ assert identify[0]["image"].get("depth") == 2, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "2"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "2"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "0"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "0 (Grayscale)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_gray2_png
+
+
+@pytest.fixture(scope="session")
+def png_gray4_img(tmp_path_factory, tmp_gray4_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_gray4_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Grayscale", str(identify)
+ assert identify[0]["image"].get("depth") == 4, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "4"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "4"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "0"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "0 (Grayscale)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_gray4_png
+
+
+@pytest.fixture(scope="session")
+def png_gray8_img(tmp_path_factory, tmp_gray8_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_gray8_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Grayscale", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "0"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "0 (Grayscale)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_gray8_png
+
+
+@pytest.fixture(scope="session")
+def png_gray16_img(tmp_path_factory, tmp_gray16_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_gray16_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Grayscale", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig")
+ == "16"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "16"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "0"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "0 (Grayscale)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_gray16_png
+
+
+@pytest.fixture(scope="session")
+def png_palette1_img(tmp_path_factory, tmp_palette1_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_palette1_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "1"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "1"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "3"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "3 (Indexed)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_palette1_png
+
+
+@pytest.fixture(scope="session")
+def png_palette2_img(tmp_path_factory, tmp_palette2_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_palette2_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "2"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "2"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "3"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "3 (Indexed)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_palette2_png
+
+
+@pytest.fixture(scope="session")
+def png_palette4_img(tmp_path_factory, tmp_palette4_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_palette4_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "4"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "4"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "3"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "3 (Indexed)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_palette4_png
+
+
+@pytest.fixture(scope="session")
+def png_palette8_img(tmp_path_factory, tmp_palette8_png):
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(tmp_palette8_png), "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "3"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "3 (Indexed)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return tmp_palette8_png
+
+
+@pytest.fixture(scope="session")
+def gif_transparent_img(tmp_path_factory, tmp_alpha_png):
+ in_img = tmp_path_factory.mktemp("gif_transparent_img") / "in.gif"
+ subprocess.check_call(CONVERT + [str(tmp_alpha_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "GIF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/gif", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "PaletteAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 256, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "LZW", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def gif_palette1_img(tmp_path_factory, tmp_palette1_png):
+ in_img = tmp_path_factory.mktemp("gif_palette1_img") / "in.gif"
+ subprocess.check_call(CONVERT + [str(tmp_palette1_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "GIF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/gif", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 2, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "LZW", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def gif_palette2_img(tmp_path_factory, tmp_palette2_png):
+ in_img = tmp_path_factory.mktemp("gif_palette2_img") / "in.gif"
+ subprocess.check_call(CONVERT + [str(tmp_palette2_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "GIF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/gif", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 4, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "LZW", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def gif_palette4_img(tmp_path_factory, tmp_palette4_png):
+ in_img = tmp_path_factory.mktemp("gif_palette4_img") / "in.gif"
+ subprocess.check_call(CONVERT + [str(tmp_palette4_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "GIF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/gif", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "LZW", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def gif_palette8_img(tmp_path_factory, tmp_palette8_png):
+ in_img = tmp_path_factory.mktemp("gif_palette8_img") / "in.gif"
+ subprocess.check_call(CONVERT + [str(tmp_palette8_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "GIF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/gif", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 256, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "LZW", str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def gif_animation_img(tmp_path_factory, tmp_normal_png, tmp_inverse_png):
+ in_img = tmp_path_factory.mktemp("gif_animation_img") / "in.gif"
+ pal_img = tmp_path_factory.mktemp("gif_animation_img") / "pal.gif"
+ tmp_img = tmp_path_factory.mktemp("gif_animation_img") / "tmp.gif"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal_png),
+ str(tmp_inverse_png),
+ str(tmp_img),
+ ]
+ )
+ # create palette image with all unique colors
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_img),
+ "-unique-colors",
+ str(pal_img),
+ ]
+ )
+ # make sure all frames have the same palette by using -remap
+ subprocess.check_call(
+ CONVERT + [str(tmp_img), "-strip", "-remap", str(pal_img), str(in_img)]
+ )
+ pal_img.unlink()
+ tmp_img.unlink()
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(in_img) + "[0]", "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "GIF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/gif", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 256, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "LZW", str(identify)
+ colormap_frame0 = identify[0]["image"].get("colormap")
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(in_img) + "[1]", "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "GIF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/gif", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 256, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "LZW", str(identify)
+ assert identify[0]["image"].get("scene") == 1, str(identify)
+ colormap_frame1 = identify[0]["image"].get("colormap")
+ assert colormap_frame0 == colormap_frame1
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_float_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("tiff_float_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal_png),
+ "-depth",
+ "32",
+ "-define",
+ "quantum:format=floating-point",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("baseDepth") == 32, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("quantum:format")
+ == "floating-point"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_cmyk8_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("tiff_cmyk8") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal_png),
+ "-colorspace",
+ "cmyk",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "CMYK", str(identify)
+ assert identify[0]["image"].get("type") == "ColorSeparation", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "separated"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_cmyk16_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("tiff_cmyk16") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal_png),
+ "-depth",
+ "16",
+ "-colorspace",
+ "cmyk",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "CMYK", str(identify)
+ assert identify[0]["image"].get("type") == "ColorSeparation", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "separated"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_rgb8_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("tiff_rgb8") / "in.tiff"
+ subprocess.check_call(
+ CONVERT + [str(tmp_normal_png), "-compress", "Zip", str(in_img)]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_rgb12_img(tmp_path_factory, tmp_normal16_png):
+ in_img = tmp_path_factory.mktemp("tiff_rgb8") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal16_png),
+ "-depth",
+ "12",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("baseDepth") == 12, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_rgb14_img(tmp_path_factory, tmp_normal16_png):
+ in_img = tmp_path_factory.mktemp("tiff_rgb8") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal16_png),
+ "-depth",
+ "14",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("baseDepth") == 14, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_rgb16_img(tmp_path_factory, tmp_normal16_png):
+ in_img = tmp_path_factory.mktemp("tiff_rgb8") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal16_png),
+ "-depth",
+ "16",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_rgba8_img(tmp_path_factory, tmp_alpha_png):
+ in_img = tmp_path_factory.mktemp("tiff_rgba8") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_alpha_png),
+ "-depth",
+ "8",
+ "-strip",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColorAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unassociated"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_rgba16_img(tmp_path_factory, tmp_alpha_png):
+ in_img = tmp_path_factory.mktemp("tiff_rgba16") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_alpha_png),
+ "-depth",
+ "16",
+ "-strip",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColorAlpha", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unassociated"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_gray1_img(tmp_path_factory, tmp_gray1_png):
+ in_img = tmp_path_factory.mktemp("tiff_gray1") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray1_png),
+ "-depth",
+ "1",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Bilevel", str(identify)
+ assert identify[0]["image"].get("depth") == 1, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-black"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_gray2_img(tmp_path_factory, tmp_gray2_png):
+ in_img = tmp_path_factory.mktemp("tiff_gray2") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray2_png),
+ "-depth",
+ "2",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Grayscale", str(identify)
+ assert identify[0]["image"].get("depth") == 2, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-black"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_gray4_img(tmp_path_factory, tmp_gray4_png):
+ in_img = tmp_path_factory.mktemp("tiff_gray4") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray4_png),
+ "-depth",
+ "4",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Grayscale", str(identify)
+ assert identify[0]["image"].get("depth") == 4, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-black"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_gray8_img(tmp_path_factory, tmp_gray8_png):
+ in_img = tmp_path_factory.mktemp("tiff_gray8") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray8_png),
+ "-depth",
+ "8",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Grayscale", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-black"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_gray16_img(tmp_path_factory, tmp_gray16_png):
+ in_img = tmp_path_factory.mktemp("tiff_gray16") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray16_png),
+ "-depth",
+ "16",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Grayscale", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-black"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_multipage_img(tmp_path_factory, tmp_normal_png, tmp_inverse_png):
+ in_img = tmp_path_factory.mktemp("tiff_multipage_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal_png),
+ str(tmp_inverse_png),
+ "-strip",
+ "-compress",
+ "Zip",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(in_img) + "[0]", "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ identify = json.loads(
+ subprocess.check_output(CONVERT + [str(in_img) + "[1]", "json:"])
+ )
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "RGB"
+ ), str(identify)
+ assert identify[0]["image"].get("scene") == 1, str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_palette1_img(tmp_path_factory, tmp_palette1_png):
+ in_img = tmp_path_factory.mktemp("tiff_palette1_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT + [str(tmp_palette1_png), "-compress", "Zip", str(in_img)]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("baseDepth") == 1, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 2, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "palette"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_palette2_img(tmp_path_factory, tmp_palette2_png):
+ in_img = tmp_path_factory.mktemp("tiff_palette2_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT + [str(tmp_palette2_png), "-compress", "Zip", str(in_img)]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("baseDepth") == 2, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 4, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "palette"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_palette4_img(tmp_path_factory, tmp_palette4_png):
+ in_img = tmp_path_factory.mktemp("tiff_palette4_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT + [str(tmp_palette4_png), "-compress", "Zip", str(in_img)]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("baseDepth") == 4, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "palette"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_palette8_img(tmp_path_factory, tmp_palette8_png):
+ in_img = tmp_path_factory.mktemp("tiff_palette8_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT + [str(tmp_palette8_png), "-compress", "Zip", str(in_img)]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "Palette", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("colormapEntries") == 256, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric") == "palette"
+ ), str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_ccitt_lsb_m2l_white_img(tmp_path_factory, tmp_gray1_png):
+ in_img = tmp_path_factory.mktemp("tiff_ccitt_lsb_m2l_white_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray1_png),
+ "-compress",
+ "group4",
+ "-define",
+ "tiff:endian=lsb",
+ "-define",
+ "tiff:fill-order=msb",
+ "-define",
+ "quantum:polarity=min-is-white",
+ "-compress",
+ "Group4",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Bilevel", str(identify)
+ endian = "endianess" if identify[0].get("version", "0") < "1.0" else "endianness"
+ assert identify[0]["image"].get(endian) in [
+ "Undefined",
+ "LSB",
+ ], str(identify)
+ assert identify[0]["image"].get("depth") == 1, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "Group4", str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert identify[0]["image"].get("properties", {}).get("tiff:endian") == "lsb", str(
+ identify
+ )
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-white"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:rows-per-strip") == "60"
+ ), str(identify)
+ tiffinfo = subprocess.check_output(["tiffinfo", str(in_img)])
+ expected = [
+ r"^ Image Width: 60 Image Length: 60",
+ r"^ Bits/Sample: 1",
+ r"^ Compression Scheme: CCITT Group 4",
+ r"^ Photometric Interpretation: min-is-white",
+ r"^ FillOrder: msb-to-lsb",
+ r"^ Samples/Pixel: 1",
+ r"^ Rows/Strip: 60",
+ ]
+ for e in expected:
+ assert re.search(e, tiffinfo.decode("utf8"), re.MULTILINE), identify.decode(
+ "utf8"
+ )
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_ccitt_msb_m2l_white_img(tmp_path_factory, tmp_gray1_png):
+ in_img = tmp_path_factory.mktemp("tiff_ccitt_msb_m2l_white_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray1_png),
+ "-compress",
+ "group4",
+ "-define",
+ "tiff:endian=msb",
+ "-define",
+ "tiff:fill-order=msb",
+ "-define",
+ "quantum:polarity=min-is-white",
+ "-compress",
+ "Group4",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Bilevel", str(identify)
+ endian = "endianess" if identify[0].get("version", "0") < "1.0" else "endianness"
+ assert identify[0]["image"].get(endian) in [
+ "Undefined",
+ "MSB",
+ ] # FIXME: should be MSB
+ assert identify[0]["image"].get("depth") == 1, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "Group4", str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert identify[0]["image"].get("properties", {}).get("tiff:endian") == "msb", str(
+ identify
+ )
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-white"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:rows-per-strip") == "60"
+ ), str(identify)
+ tiffinfo = subprocess.check_output(["tiffinfo", str(in_img)])
+ expected = [
+ r"^ Image Width: 60 Image Length: 60",
+ r"^ Bits/Sample: 1",
+ r"^ Compression Scheme: CCITT Group 4",
+ r"^ Photometric Interpretation: min-is-white",
+ r"^ FillOrder: msb-to-lsb",
+ r"^ Samples/Pixel: 1",
+ r"^ Rows/Strip: 60",
+ ]
+ for e in expected:
+ assert re.search(e, tiffinfo.decode("utf8"), re.MULTILINE), identify.decode(
+ "utf8"
+ )
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_ccitt_msb_l2m_white_img(tmp_path_factory, tmp_gray1_png):
+ in_img = tmp_path_factory.mktemp("tiff_ccitt_msb_l2m_white_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray1_png),
+ "-compress",
+ "group4",
+ "-define",
+ "tiff:endian=msb",
+ "-define",
+ "tiff:fill-order=lsb",
+ "-define",
+ "quantum:polarity=min-is-white",
+ "-compress",
+ "Group4",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Bilevel", str(identify)
+ endian = "endianess" if identify[0].get("version", "0") < "1.0" else "endianness"
+ assert identify[0]["image"].get(endian) in [
+ "Undefined",
+ "MSB",
+ ] # FIXME: should be MSB
+ assert identify[0]["image"].get("depth") == 1, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "Group4", str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert identify[0]["image"].get("properties", {}).get("tiff:endian") == "msb", str(
+ identify
+ )
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-white"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:rows-per-strip") == "60"
+ ), str(identify)
+ tiffinfo = subprocess.check_output(["tiffinfo", str(in_img)])
+ expected = [
+ r"^ Image Width: 60 Image Length: 60",
+ r"^ Bits/Sample: 1",
+ r"^ Compression Scheme: CCITT Group 4",
+ r"^ Photometric Interpretation: min-is-white",
+ r"^ FillOrder: lsb-to-msb",
+ r"^ Samples/Pixel: 1",
+ r"^ Rows/Strip: 60",
+ ]
+ for e in expected:
+ assert re.search(e, tiffinfo.decode("utf8"), re.MULTILINE), identify.decode(
+ "utf8"
+ )
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_ccitt_lsb_m2l_black_img(tmp_path_factory, tmp_gray1_png):
+ in_img = tmp_path_factory.mktemp("tiff_ccitt_lsb_m2l_black_img") / "in.tiff"
+ # "-define quantum:polarity=min-is-black" requires ImageMagick with:
+ # https://github.com/ImageMagick/ImageMagick/commit/00730551f0a34328685c59d0dde87dd9e366103a
+ # or at least 7.0.8-11 from Aug 29, 2018
+ # or at least 6.9.10-12 from Sep 7, 2018 (for the ImageMagick6 branch)
+ # also see: https://www.imagemagick.org/discourse-server/viewtopic.php?f=1&t=34605
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray1_png),
+ "-compress",
+ "group4",
+ "-define",
+ "tiff:endian=lsb",
+ "-define",
+ "tiff:fill-order=msb",
+ "-define",
+ "quantum:polarity=min-is-black",
+ "-compress",
+ "Group4",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Bilevel", str(identify)
+ endian = "endianess" if identify[0].get("version", "0") < "1.0" else "endianness"
+ assert identify[0]["image"].get(endian) in [
+ "Undefined",
+ "LSB",
+ ], str(identify)
+ assert identify[0]["image"].get("depth") == 1, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "Group4", str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert identify[0]["image"].get("properties", {}).get("tiff:endian") == "lsb", str(
+ identify
+ )
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-black"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:rows-per-strip") == "60"
+ ), str(identify)
+ tiffinfo = subprocess.check_output(["tiffinfo", str(in_img)])
+ expected = [
+ r"^ Image Width: 60 Image Length: 60",
+ r"^ Bits/Sample: 1",
+ r"^ Compression Scheme: CCITT Group 4",
+ r"^ Photometric Interpretation: min-is-black",
+ r"^ FillOrder: msb-to-lsb",
+ r"^ Samples/Pixel: 1",
+ r"^ Rows/Strip: 60",
+ ]
+ for e in expected:
+ assert re.search(e, tiffinfo.decode("utf8"), re.MULTILINE), identify.decode(
+ "utf8"
+ )
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_ccitt_nometa1_img(tmp_path_factory, tmp_gray1_png):
+ in_img = tmp_path_factory.mktemp("tiff_ccitt_nometa1_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray1_png),
+ "-compress",
+ "group4",
+ "-define",
+ "tiff:endian=lsb",
+ "-define",
+ "tiff:fill-order=msb",
+ "-define",
+ "quantum:polarity=min-is-white",
+ "-compress",
+ "Group4",
+ str(in_img),
+ ]
+ )
+ subprocess.check_call(
+ ["tiffset", "-u", "258", str(in_img)]
+ ) # remove BitsPerSample (258)
+ subprocess.check_call(
+ ["tiffset", "-u", "266", str(in_img)]
+ ) # remove FillOrder (266)
+ subprocess.check_call(
+ ["tiffset", "-u", "277", str(in_img)]
+ ) # remove SamplesPerPixel (277)
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("type") == "Bilevel", str(identify)
+ endian = "endianess" if identify[0].get("version", "0") < "1.0" else "endianness"
+ assert identify[0]["image"].get(endian) in [
+ "Undefined",
+ "LSB",
+ ], str(identify)
+ assert identify[0]["image"].get("depth") == 1, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("compression") == "Group4", str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert identify[0]["image"].get("properties", {}).get("tiff:endian") == "lsb", str(
+ identify
+ )
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-white"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:rows-per-strip") == "60"
+ ), str(identify)
+ tiffinfo = subprocess.check_output(["tiffinfo", str(in_img)])
+ expected = [
+ r"^ Image Width: 60 Image Length: 60",
+ r"^ Compression Scheme: CCITT Group 4",
+ r"^ Photometric Interpretation: min-is-white",
+ r"^ Rows/Strip: 60",
+ ]
+ for e in expected:
+ assert re.search(e, tiffinfo.decode("utf8"), re.MULTILINE), identify.decode(
+ "utf8"
+ )
+ unexpected = [" Bits/Sample: ", " FillOrder: ", " Samples/Pixel: "]
+ for e in unexpected:
+ assert e not in tiffinfo.decode("utf8")
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def tiff_ccitt_nometa2_img(tmp_path_factory, tmp_gray1_png):
+ in_img = tmp_path_factory.mktemp("tiff_ccitt_nometa2_img") / "in.tiff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_gray1_png),
+ "-compress",
+ "group4",
+ "-define",
+ "tiff:endian=lsb",
+ "-define",
+ "tiff:fill-order=msb",
+ "-define",
+ "quantum:polarity=min-is-white",
+ "-compress",
+ "Group4",
+ str(in_img),
+ ]
+ )
+ subprocess.check_call(
+ ["tiffset", "-u", "278", str(in_img)]
+ ) # remove RowsPerStrip (278)
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "TIFF", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/tiff", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("units") == "PixelsPerInch", str(identify)
+ assert identify[0]["image"].get("type") == "Bilevel", str(identify)
+ endian = "endianess" if identify[0].get("version", "0") < "1.0" else "endianness"
+ assert identify[0]["image"].get(endian) in [
+ "Undefined",
+ "LSB",
+ ], str(identify)
+ assert identify[0]["image"].get("colorspace") == "Gray", str(identify)
+ assert identify[0]["image"].get("depth") == 1, str(identify)
+ assert identify[0]["image"].get("compression") == "Group4", str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:alpha") == "unspecified"
+ ), str(identify)
+ assert identify[0]["image"].get("properties", {}).get("tiff:endian") == "lsb", str(
+ identify
+ )
+ assert (
+ identify[0]["image"].get("properties", {}).get("tiff:photometric")
+ == "min-is-white"
+ ), str(identify)
+ assert "tiff:rows-per-strip" not in identify[0]["image"]["properties"]
+ tiffinfo = subprocess.check_output(["tiffinfo", str(in_img)])
+ expected = [
+ r"^ Image Width: 60 Image Length: 60",
+ r"^ Bits/Sample: 1",
+ r"^ Compression Scheme: CCITT Group 4",
+ r"^ Photometric Interpretation: min-is-white",
+ r"^ FillOrder: msb-to-lsb",
+ r"^ Samples/Pixel: 1",
+ ]
+ for e in expected:
+ assert re.search(e, tiffinfo.decode("utf8"), re.MULTILINE), identify.decode(
+ "utf8"
+ )
+ unexpected = [" Rows/Strip: "]
+ for e in unexpected:
+ assert e not in tiffinfo.decode("utf8")
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def miff_cmyk8_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("miff_cmyk8") / "in.miff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal_png),
+ "-colorspace",
+ "cmyk",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "MIFF", str(identify)
+ assert identify[0]["image"].get("class") == "DirectClass"
+ assert identify[0]["image"].get("type") == "ColorSeparation"
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "CMYK", str(identify)
+ assert identify[0]["image"].get("type") == "ColorSeparation", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def miff_cmyk16_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("miff_cmyk16") / "in.miff"
+ subprocess.check_call(
+ CONVERT
+ + [
+ str(tmp_normal_png),
+ "-depth",
+ "16",
+ "-colorspace",
+ "cmyk",
+ str(in_img),
+ ]
+ )
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "MIFF", str(identify)
+ assert identify[0]["image"].get("class") == "DirectClass"
+ assert identify[0]["image"].get("type") == "ColorSeparation"
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "CMYK", str(identify)
+ assert identify[0]["image"].get("type") == "ColorSeparation", str(identify)
+ assert identify[0]["image"].get("depth") == 16, str(identify)
+ assert identify[0]["image"].get("baseDepth") == 16, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def miff_rgb8_img(tmp_path_factory, tmp_normal_png):
+ in_img = tmp_path_factory.mktemp("miff_rgb8") / "in.miff"
+ subprocess.check_call(CONVERT + [str(tmp_normal_png), str(in_img)])
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "MIFF", str(identify)
+ assert identify[0]["image"].get("class") == "DirectClass"
+ assert identify[0]["image"].get("type") == "TrueColor"
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ yield in_img
+ in_img.unlink()
+
+
+@pytest.fixture(scope="session")
+def png_icc_img(tmp_icc_png):
+ in_img = tmp_icc_png
+ identify = json.loads(subprocess.check_output(CONVERT + [str(in_img), "json:"]))
+ assert len(identify) == 1
+ # somewhere between imagemagick 6.9.7.4 and 6.9.9.34, the json output was
+ # put into an array, here we cater for the older version containing just
+ # the bare dictionary
+ if "image" in identify:
+ identify = [identify]
+ assert "image" in identify[0]
+ assert identify[0]["image"].get("format") == "PNG", str(identify)
+ assert identify[0]["image"].get("mimeType") == "image/png", str(identify)
+ assert identify[0]["image"].get("geometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert identify[0]["image"].get("colorspace") == "sRGB", str(identify)
+ assert identify[0]["image"].get("type") == "TrueColor", str(identify)
+ assert identify[0]["image"].get("depth") == 8, str(identify)
+ assert identify[0]["image"].get("pageGeometry") == {
+ "width": 60,
+ "height": 60,
+ "x": 0,
+ "y": 0,
+ }, str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit-depth-orig") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.bit_depth") == "8"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color-type-orig")
+ == "2"
+ ), str(identify)
+ assert (
+ identify[0]["image"].get("properties", {}).get("png:IHDR.color_type")
+ == "2 (Truecolor)"
+ ), str(identify)
+ assert (
+ identify[0]["image"]["properties"]["png:IHDR.interlace_method"]
+ == "0 (Not interlaced)"
+ ), str(identify)
+ return in_img
+
+
+###############################################################################
+# OUTPUT FIXTURES #
+###############################################################################
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def jpg_pdf(tmp_path_factory, jpg_img, request):
+ out_pdf = tmp_path_factory.mktemp("jpg_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(jpg_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/DCTDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def jpg_rot_pdf(tmp_path_factory, jpg_rot_img, request):
+ out_pdf = tmp_path_factory.mktemp("jpg_rot_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(jpg_rot_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/DCTDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ assert p.pages[0].Rotate == 90
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def jpg_cmyk_pdf(tmp_path_factory, jpg_cmyk_img, request):
+ out_pdf = tmp_path_factory.mktemp("jpg_cmyk_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(jpg_cmyk_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceCMYK"
+ assert p.pages[0].Resources.XObject.Im0.Decode == pikepdf.Array(
+ [1, 0, 1, 0, 1, 0, 1, 0]
+ )
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/DCTDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def jpg_2000_pdf(tmp_path_factory, jpg_2000_img, request):
+ out_pdf = tmp_path_factory.mktemp("jpg_2000_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ jpg_2000_img,
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/JPXDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def jpg_2000_rgba8_pdf(tmp_path_factory, jpg_2000_rgba8_img, request):
+ out_pdf = tmp_path_factory.mktemp("jpg_2000_rgba8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ jpg_2000_rgba8_img,
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert not hasattr(p.pages[0].Resources.XObject.Im0, "ColorSpace")
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/JPXDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def jpg_2000_rgba16_pdf(tmp_path_factory, jpg_2000_rgba16_img, request):
+ out_pdf = tmp_path_factory.mktemp("jpg_2000_rgba16_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ jpg_2000_rgba16_img,
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 16
+ assert not hasattr(p.pages[0].Resources.XObject.Im0, "ColorSpace")
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/JPXDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_rgb8_pdf(tmp_path_factory, png_rgb8_img, request):
+ out_pdf = tmp_path_factory.mktemp("png_rgb8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(png_rgb8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_rgba8_pdf(tmp_path_factory, png_rgba8_img, request):
+ out_pdf = tmp_path_factory.mktemp("png_rgba8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(png_rgba8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ assert p.pages[0].Resources.XObject.Im0.SMask is not None
+
+ assert p.pages[0].Resources.XObject.Im0.SMask.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.SMask.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.SMask.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.SMask.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.SMask.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def gif_transparent_pdf(tmp_path_factory, gif_transparent_img, request):
+ out_pdf = tmp_path_factory.mktemp("gif_transparent_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(gif_transparent_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ assert p.pages[0].Resources.XObject.Im0.SMask is not None
+
+ assert p.pages[0].Resources.XObject.Im0.SMask.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.SMask.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.SMask.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.SMask.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.SMask.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_rgb16_pdf(tmp_path_factory, png_rgb16_img, request):
+ out_pdf = tmp_path_factory.mktemp("png_rgb16_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(png_rgb16_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 16
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 16
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_interlaced_pdf(tmp_path_factory, png_interlaced_img, request):
+ out_pdf = tmp_path_factory.mktemp("png_interlaced_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(png_interlaced_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_gray1_pdf(tmp_path_factory, tmp_gray1_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_gray1_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_gray1_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_gray2_pdf(tmp_path_factory, tmp_gray2_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_gray2_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_gray2_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 2
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 2
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_gray4_pdf(tmp_path_factory, tmp_gray4_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_gray4_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_gray4_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 4
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 4
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_gray8_pdf(tmp_path_factory, tmp_gray8_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_gray8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_gray8_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_gray8a_pdf(tmp_path_factory, png_gray8a_img, request):
+ out_pdf = tmp_path_factory.mktemp("png_gray8a_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(png_gray8a_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ assert p.pages[0].Resources.XObject.Im0.SMask is not None
+
+ assert p.pages[0].Resources.XObject.Im0.SMask.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.SMask.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.SMask.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.SMask.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.SMask.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.SMask.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_gray16_pdf(tmp_path_factory, tmp_gray16_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_gray16_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_gray16_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 16
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 16
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_palette1_pdf(tmp_path_factory, tmp_palette1_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_palette1_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_palette1_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_palette2_pdf(tmp_path_factory, tmp_palette2_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_palette2_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_palette2_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 2
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 2
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_palette4_pdf(tmp_path_factory, tmp_palette4_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_palette4_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_palette4_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 4
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 4
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_palette8_pdf(tmp_path_factory, tmp_palette8_png, request):
+ out_pdf = tmp_path_factory.mktemp("png_palette8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_palette8_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def png_icc_pdf(tmp_path_factory, tmp_icc_png, tmp_icc_profile, request):
+ out_pdf = tmp_path_factory.mktemp("png_icc_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tmp_icc_png),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/ICCBased"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1].N == 3
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1].Alternate == "/DeviceRGB"
+ assert (
+ p.pages[0].Resources.XObject.Im0.ColorSpace[1].read_bytes()
+ == tmp_icc_profile.read_bytes()
+ )
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def gif_palette1_pdf(tmp_path_factory, gif_palette1_img, request):
+ out_pdf = tmp_path_factory.mktemp("gif_palette1_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(gif_palette1_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def gif_palette2_pdf(tmp_path_factory, gif_palette2_img, request):
+ out_pdf = tmp_path_factory.mktemp("gif_palette2_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(gif_palette2_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 2
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 2
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def gif_palette4_pdf(tmp_path_factory, gif_palette4_img, request):
+ out_pdf = tmp_path_factory.mktemp("gif_palette4_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(gif_palette4_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 4
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 4
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def gif_palette8_pdf(tmp_path_factory, gif_palette8_img, request):
+ out_pdf = tmp_path_factory.mktemp("gif_palette8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(gif_palette8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def gif_animation_pdf(tmp_path_factory, gif_animation_img, request):
+ tmpdir = tmp_path_factory.mktemp("gif_animation_pdf")
+ out_pdf = tmpdir / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(gif_animation_img),
+ ]
+ )
+ pdfinfo = subprocess.check_output(["pdfinfo", str(out_pdf)])
+ assert re.search(
+ "^Pages: +2$", pdfinfo.decode("utf8"), re.MULTILINE
+ ), identify.decode("utf8")
+ subprocess.check_call(["pdfseparate", str(out_pdf), str(tmpdir / "page-%d.pdf")])
+ for page in [1, 2]:
+ gif_animation_pdf_nr = tmpdir / ("page-%d.pdf" % page)
+ with pikepdf.open(gif_animation_pdf_nr) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ gif_animation_pdf_nr.unlink()
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_cmyk8_pdf(tmp_path_factory, tiff_cmyk8_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_cmyk8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_cmyk8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceCMYK"
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_rgb8_pdf(tmp_path_factory, tiff_rgb8_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_rgb8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_rgb8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_gray1_pdf(tmp_path_factory, tiff_gray1_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_gray1_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_gray1_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].BlackIs1 == True
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Columns == 60
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].K == -1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Rows == 60
+ assert p.pages[0].Resources.XObject.Im0.Filter[0] == "/CCITTFaxDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_gray2_pdf(tmp_path_factory, tiff_gray2_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_gray2_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_gray2_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_gray4_pdf(tmp_path_factory, tiff_gray4_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_gray4_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_gray4_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_gray8_pdf(tmp_path_factory, tiff_gray8_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_gray8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_gray8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_multipage_pdf(tmp_path_factory, tiff_multipage_img, request):
+ tmpdir = tmp_path_factory.mktemp("tiff_multipage_pdf")
+ out_pdf = tmpdir / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_multipage_img),
+ ]
+ )
+ pdfinfo = subprocess.check_output(["pdfinfo", str(out_pdf)])
+ assert re.search(
+ "^Pages: +2$", pdfinfo.decode("utf8"), re.MULTILINE
+ ), identify.decode("utf8")
+ subprocess.check_call(["pdfseparate", str(out_pdf), str(tmpdir / "page-%d.pdf")])
+ for page in [1, 2]:
+ tiff_multipage_pdf_nr = tmpdir / ("page-%d.pdf" % page)
+ with pikepdf.open(tiff_multipage_pdf_nr) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ tiff_multipage_pdf_nr.unlink()
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_palette1_pdf(tmp_path_factory, tiff_palette1_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_palette1_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_palette1_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_palette2_pdf(tmp_path_factory, tiff_palette2_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_palette2_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_palette2_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 2
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 2
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_palette4_pdf(tmp_path_factory, tiff_palette4_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_palette4_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_palette4_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 4
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 4
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_palette8_pdf(tmp_path_factory, tiff_palette8_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_palette8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_palette8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[0] == "/Indexed"
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace[1] == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_ccitt_lsb_m2l_white_pdf(
+ tmp_path_factory, tiff_ccitt_lsb_m2l_white_img, request
+):
+ out_pdf = tmp_path_factory.mktemp("tiff_ccitt_lsb_m2l_white_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_ccitt_lsb_m2l_white_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].BlackIs1 == False
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Columns == 60
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].K == -1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Rows == 60
+ assert p.pages[0].Resources.XObject.Im0.Filter[0] == "/CCITTFaxDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_ccitt_msb_m2l_white_pdf(
+ tmp_path_factory, tiff_ccitt_msb_m2l_white_img, request
+):
+ out_pdf = tmp_path_factory.mktemp("tiff_ccitt_msb_m2l_white_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_ccitt_msb_m2l_white_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].BlackIs1 == False
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Columns == 60
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].K == -1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Rows == 60
+ assert p.pages[0].Resources.XObject.Im0.Filter[0] == "/CCITTFaxDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_ccitt_msb_l2m_white_pdf(
+ tmp_path_factory, tiff_ccitt_msb_l2m_white_img, request
+):
+ out_pdf = tmp_path_factory.mktemp("tiff_ccitt_msb_l2m_white_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_ccitt_msb_l2m_white_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].BlackIs1 == False
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Columns == 60
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].K == -1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Rows == 60
+ assert p.pages[0].Resources.XObject.Im0.Filter[0] == "/CCITTFaxDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_ccitt_lsb_m2l_black_pdf(
+ tmp_path_factory, tiff_ccitt_lsb_m2l_black_img, request
+):
+ out_pdf = tmp_path_factory.mktemp("tiff_ccitt_lsb_m2l_black_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_ccitt_lsb_m2l_black_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].BlackIs1 == True
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Columns == 60
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].K == -1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Rows == 60
+ assert p.pages[0].Resources.XObject.Im0.Filter[0] == "/CCITTFaxDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_ccitt_nometa1_pdf(tmp_path_factory, tiff_ccitt_nometa1_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_ccitt_nometa1_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_ccitt_nometa1_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].BlackIs1 == False
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Columns == 60
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].K == -1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Rows == 60
+ assert p.pages[0].Resources.XObject.Im0.Filter[0] == "/CCITTFaxDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def tiff_ccitt_nometa2_pdf(tmp_path_factory, tiff_ccitt_nometa2_img, request):
+ out_pdf = tmp_path_factory.mktemp("tiff_ccitt_nometa2_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(tiff_ccitt_nometa2_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 1
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceGray"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].BlackIs1 == False
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Columns == 60
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].K == -1
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms[0].Rows == 60
+ assert p.pages[0].Resources.XObject.Im0.Filter[0] == "/CCITTFaxDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def miff_cmyk8_pdf(tmp_path_factory, miff_cmyk8_img, request):
+ out_pdf = tmp_path_factory.mktemp("miff_cmyk8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(miff_cmyk8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceCMYK"
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def miff_cmyk16_pdf(tmp_path_factory, miff_cmyk16_img, request):
+ out_pdf = tmp_path_factory.mktemp("miff_cmyk16_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(miff_cmyk16_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 16
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceCMYK"
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+@pytest.fixture(scope="session", params=["internal", "pikepdf"])
+def miff_rgb8_pdf(tmp_path_factory, miff_rgb8_img, request):
+ out_pdf = tmp_path_factory.mktemp("miff_rgb8_pdf") / "out.pdf"
+ subprocess.check_call(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + request.param,
+ "--output=" + str(out_pdf),
+ str(miff_rgb8_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert (
+ p.pages[0].Contents.read_bytes()
+ == b"q\n45.0000 0 0 45.0000 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ )
+ assert p.pages[0].Resources.XObject.Im0.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.ColorSpace == "/DeviceRGB"
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.BitsPerComponent == 8
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Colors == 3
+ assert p.pages[0].Resources.XObject.Im0.DecodeParms.Predictor == 15
+ assert p.pages[0].Resources.XObject.Im0.Filter == "/FlateDecode"
+ assert p.pages[0].Resources.XObject.Im0.Height == 60
+ assert p.pages[0].Resources.XObject.Im0.Width == 60
+ yield out_pdf
+ out_pdf.unlink()
+
+
+###############################################################################
+# TEST CASES #
+###############################################################################
+
+
+@pytest.mark.skipif(
+ sys.platform in ["darwin", "win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_jpg(tmp_path_factory, jpg_img, jpg_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg")
+ pnm = tmpdir / "jpg.pnm"
+ # We have to use jpegtopnm with the original JPG before being able to compare
+ # it with imagemagick because imagemagick will decode the JPG slightly
+ # differently than ghostscript, poppler and mupdf do it.
+ # We have to use jpegtopnm and cannot use djpeg because the latter produces
+ # slightly different results as well when called like this:
+ # djpeg -dct int -pnm "$tempdir/normal.jpg" > "$tempdir/normal.pnm"
+ # An alternative way to compare the JPG would be to require a different DCT
+ # method when decoding by setting -define jpeg:dct-method=ifast in the
+ # compare command.
+ pnm.write_bytes(subprocess.check_output(["jpegtopnm", "-dct", "int", str(jpg_img)]))
+ compare_ghostscript(tmpdir, pnm, jpg_pdf)
+ compare_poppler(tmpdir, pnm, jpg_pdf)
+ compare_mupdf(tmpdir, pnm, jpg_pdf)
+ pnm.unlink()
+ compare_pdfimages_jpg(tmpdir, jpg_img, jpg_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["darwin", "win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_jpg_rot(tmp_path_factory, jpg_rot_img, jpg_rot_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg_rot")
+ # We have to use jpegtopnm with the original JPG before being able to compare
+ # it with imagemagick because imagemagick will decode the JPG slightly
+ # differently than ghostscript, poppler and mupdf do it.
+ # We have to use jpegtopnm and cannot use djpeg because the latter produces
+ # slightly different results as well when called like this:
+ # djpeg -dct int -pnm "$tempdir/normal.jpg" > "$tempdir/normal.pnm"
+ # An alternative way to compare the JPG would be to require a different DCT
+ # method when decoding by setting -define jpeg:dct-method=ifast in the
+ # compare command.
+ jpg_rot_pnm = tmpdir / "jpg_rot.pnm"
+ jpg_rot_pnm.write_bytes(
+ subprocess.check_output(["jpegtopnm", "-dct", "int", str(jpg_rot_img)])
+ )
+ jpg_rot_png = tmpdir / "jpg_rot.png"
+ subprocess.check_call(
+ CONVERT + ["-rotate", "90", str(jpg_rot_pnm), str(jpg_rot_png)]
+ )
+ jpg_rot_pnm.unlink()
+ compare_ghostscript(tmpdir, jpg_rot_png, jpg_rot_pdf)
+ compare_poppler(tmpdir, jpg_rot_png, jpg_rot_pdf)
+ compare_mupdf(tmpdir, jpg_rot_png, jpg_rot_pdf)
+ jpg_rot_png.unlink()
+ compare_pdfimages_jpg(tmpdir, jpg_rot_img, jpg_rot_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["darwin", "win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_jpg_cmyk(tmp_path_factory, jpg_cmyk_img, jpg_cmyk_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg_cmyk")
+ compare_ghostscript(tmpdir, jpg_cmyk_img, jpg_cmyk_pdf, gsdevice="tiff32nc")
+ # not testing with poppler as it cannot write CMYK images
+ compare_mupdf(tmpdir, jpg_cmyk_img, jpg_cmyk_pdf, cmyk=True)
+ compare_pdfimages_cmyk(tmpdir, jpg_cmyk_img, jpg_cmyk_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.skipif(
+ not HAVE_JP2, reason="requires imagemagick with support for jpeg2000"
+)
+def test_jpg_2000(tmp_path_factory, jpg_2000_img, jpg_2000_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg_2000")
+ compare_ghostscript(tmpdir, jpg_2000_img, jpg_2000_pdf)
+ compare_poppler(tmpdir, jpg_2000_img, jpg_2000_pdf)
+ compare_mupdf(tmpdir, jpg_2000_img, jpg_2000_pdf)
+ compare_pdfimages_jp2(tmpdir, jpg_2000_img, jpg_2000_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.skipif(
+ not HAVE_JP2, reason="requires imagemagick with support for jpeg2000"
+)
+@pytest.mark.skipif(
+ True, reason="https://github.com/ImageMagick/ImageMagick6/issues/285"
+)
+def test_jpg_2000_rgba8(tmp_path_factory, jpg_2000_rgba8_img, jpg_2000_rgba8_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg_2000_rgba8")
+ compare_ghostscript(tmpdir, jpg_2000_rgba8_img, jpg_2000_rgba8_pdf)
+ compare_poppler(tmpdir, jpg_2000_rgba8_img, jpg_2000_rgba8_pdf)
+ # compare_mupdf(tmpdir, jpg_2000_rgba8_img, jpg_2000_rgba8_pdf)
+ compare_pdfimages_jp2(tmpdir, jpg_2000_rgba8_img, jpg_2000_rgba8_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.skipif(
+ not HAVE_JP2, reason="requires imagemagick with support for jpeg2000"
+)
+@pytest.mark.skipif(
+ True, reason="https://github.com/ImageMagick/ImageMagick6/issues/285"
+)
+def test_jpg_2000_rgba16(tmp_path_factory, jpg_2000_rgba16_img, jpg_2000_rgba16_pdf):
+ tmpdir = tmp_path_factory.mktemp("jpg_2000_rgba16")
+ compare_ghostscript(
+ tmpdir, jpg_2000_rgba16_img, jpg_2000_rgba16_pdf, gsdevice="tiff48nc"
+ )
+ # poppler outputs 8-bit RGB so the comparison will not be exact
+ # compare_poppler(tmpdir, jpg_2000_rgba16_img, jpg_2000_rgba16_pdf, exact=False)
+ # compare_mupdf(tmpdir, jpg_2000_rgba16_img, jpg_2000_rgba16_pdf)
+ compare_pdfimages_jp2(tmpdir, jpg_2000_rgba16_img, jpg_2000_rgba16_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_rgb8(tmp_path_factory, png_rgb8_img, png_rgb8_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_rgb8")
+ compare_ghostscript(tmpdir, png_rgb8_img, png_rgb8_pdf)
+ compare_poppler(tmpdir, png_rgb8_img, png_rgb8_pdf)
+ compare_mupdf(tmpdir, png_rgb8_img, png_rgb8_pdf)
+ compare_pdfimages_png(tmpdir, png_rgb8_img, png_rgb8_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_rgb16(tmp_path_factory, png_rgb16_img, png_rgb16_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_rgb16")
+ compare_ghostscript(tmpdir, png_rgb16_img, png_rgb16_pdf, gsdevice="tiff48nc")
+ # poppler outputs 8-bit RGB so the comparison will not be exact
+ compare_poppler(tmpdir, png_rgb16_img, png_rgb16_pdf, exact=False)
+ # pdfimages is unable to write 16 bit output
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_rgba8(tmp_path_factory, png_rgba8_img, png_rgba8_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_rgba8")
+ compare_pdfimages_png(tmpdir, png_rgba8_img, png_rgba8_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_png_rgba16(tmp_path_factory, png_rgba16_img, engine):
+ out_pdf = tmp_path_factory.mktemp("png_rgba16") / "out.pdf"
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(png_rgba16_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_gray8a(tmp_path_factory, png_gray8a_img, png_gray8a_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_gray8a")
+ compare_pdfimages_png(tmpdir, png_gray8a_img, png_gray8a_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_png_gray16a(tmp_path_factory, png_gray16a_img, engine):
+ out_pdf = tmp_path_factory.mktemp("png_gray16a") / "out.pdf"
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(png_gray16a_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_interlaced(tmp_path_factory, png_interlaced_img, png_interlaced_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_interlaced")
+ compare_ghostscript(tmpdir, png_interlaced_img, png_interlaced_pdf)
+ compare_poppler(tmpdir, png_interlaced_img, png_interlaced_pdf)
+ compare_mupdf(tmpdir, png_interlaced_img, png_interlaced_pdf)
+ compare_pdfimages_png(tmpdir, png_interlaced_img, png_interlaced_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_gray1(tmp_path_factory, png_gray1_img, png_gray1_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_gray1")
+ compare_ghostscript(tmpdir, png_gray1_img, png_gray1_pdf, gsdevice="pnggray")
+ compare_poppler(tmpdir, png_gray1_img, png_gray1_pdf)
+ compare_mupdf(tmpdir, png_gray1_img, png_gray1_pdf)
+ compare_pdfimages_png(tmpdir, png_gray1_img, png_gray1_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_gray2(tmp_path_factory, png_gray2_img, png_gray2_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_gray2")
+ compare_ghostscript(tmpdir, png_gray2_img, png_gray2_pdf, gsdevice="pnggray")
+ compare_poppler(tmpdir, png_gray2_img, png_gray2_pdf)
+ compare_mupdf(tmpdir, png_gray2_img, png_gray2_pdf)
+ compare_pdfimages_png(tmpdir, png_gray2_img, png_gray2_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_gray4(tmp_path_factory, png_gray4_img, png_gray4_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_gray4")
+ compare_ghostscript(tmpdir, png_gray4_img, png_gray4_pdf, gsdevice="pnggray")
+ compare_poppler(tmpdir, png_gray4_img, png_gray4_pdf)
+ compare_mupdf(tmpdir, png_gray4_img, png_gray4_pdf)
+ compare_pdfimages_png(tmpdir, png_gray4_img, png_gray4_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_gray8(tmp_path_factory, png_gray8_img, png_gray8_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_gray8")
+ compare_ghostscript(tmpdir, png_gray8_img, png_gray8_pdf, gsdevice="pnggray")
+ compare_poppler(tmpdir, png_gray8_img, png_gray8_pdf)
+ compare_mupdf(tmpdir, png_gray8_img, png_gray8_pdf)
+ compare_pdfimages_png(tmpdir, png_gray8_img, png_gray8_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_gray16(tmp_path_factory, png_gray16_img, png_gray16_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_gray16")
+ # ghostscript outputs 8-bit grayscale, so the comparison will not be exact
+ compare_ghostscript(
+ tmpdir, png_gray16_img, png_gray16_pdf, gsdevice="pnggray", exact=False
+ )
+ # poppler outputs 8-bit grayscale so the comparison will not be exact
+ compare_poppler(tmpdir, png_gray16_img, png_gray16_pdf, exact=False)
+ # pdfimages outputs 8-bit grayscale so the comparison will not be exact
+ compare_pdfimages_png(tmpdir, png_gray16_img, png_gray16_pdf, exact=False)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_palette1(tmp_path_factory, png_palette1_img, png_palette1_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_palette1")
+ compare_ghostscript(tmpdir, png_palette1_img, png_palette1_pdf)
+ compare_poppler(tmpdir, png_palette1_img, png_palette1_pdf)
+ compare_mupdf(tmpdir, png_palette1_img, png_palette1_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_palette2(tmp_path_factory, png_palette2_img, png_palette2_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_palette2")
+ compare_ghostscript(tmpdir, png_palette2_img, png_palette2_pdf)
+ compare_poppler(tmpdir, png_palette2_img, png_palette2_pdf)
+ compare_mupdf(tmpdir, png_palette2_img, png_palette2_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_palette4(tmp_path_factory, png_palette4_img, png_palette4_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_palette4")
+ compare_ghostscript(tmpdir, png_palette4_img, png_palette4_pdf)
+ compare_poppler(tmpdir, png_palette4_img, png_palette4_pdf)
+ compare_mupdf(tmpdir, png_palette4_img, png_palette4_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_palette8(tmp_path_factory, png_palette8_img, png_palette8_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_palette8")
+ compare_ghostscript(tmpdir, png_palette8_img, png_palette8_pdf)
+ compare_poppler(tmpdir, png_palette8_img, png_palette8_pdf)
+ compare_mupdf(tmpdir, png_palette8_img, png_palette8_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["darwin", "win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_png_icc(tmp_path_factory, png_icc_img, png_icc_pdf):
+ tmpdir = tmp_path_factory.mktemp("png_icc")
+ compare_ghostscript(tmpdir, png_icc_img, png_icc_pdf, exact=False, icc=True)
+ compare_poppler(tmpdir, png_icc_img, png_icc_pdf, exact=False, icc=True)
+ # mupdf ignores the ICC profile in Debian (needs patched thirdparty liblcms2-art)
+ compare_pdfimages_png(tmpdir, png_icc_img, png_icc_pdf, exact=False, icc=True)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_gif_transparent(tmp_path_factory, gif_transparent_img, gif_transparent_pdf):
+ tmpdir = tmp_path_factory.mktemp("gif_transparent")
+ compare_pdfimages_png(tmpdir, gif_transparent_img, gif_transparent_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_gif_palette1(tmp_path_factory, gif_palette1_img, gif_palette1_pdf):
+ tmpdir = tmp_path_factory.mktemp("gif_palette1")
+ compare_ghostscript(tmpdir, gif_palette1_img, gif_palette1_pdf)
+ compare_poppler(tmpdir, gif_palette1_img, gif_palette1_pdf)
+ compare_mupdf(tmpdir, gif_palette1_img, gif_palette1_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_gif_palette2(tmp_path_factory, gif_palette2_img, gif_palette2_pdf):
+ tmpdir = tmp_path_factory.mktemp("gif_palette2")
+ compare_ghostscript(tmpdir, gif_palette2_img, gif_palette2_pdf)
+ compare_poppler(tmpdir, gif_palette2_img, gif_palette2_pdf)
+ compare_mupdf(tmpdir, gif_palette2_img, gif_palette2_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_gif_palette4(tmp_path_factory, gif_palette4_img, gif_palette4_pdf):
+ tmpdir = tmp_path_factory.mktemp("gif_palette4")
+ compare_ghostscript(tmpdir, gif_palette4_img, gif_palette4_pdf)
+ compare_poppler(tmpdir, gif_palette4_img, gif_palette4_pdf)
+ compare_mupdf(tmpdir, gif_palette4_img, gif_palette4_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_gif_palette8(tmp_path_factory, gif_palette8_img, gif_palette8_pdf):
+ tmpdir = tmp_path_factory.mktemp("gif_palette8")
+ compare_ghostscript(tmpdir, gif_palette8_img, gif_palette8_pdf)
+ compare_poppler(tmpdir, gif_palette8_img, gif_palette8_pdf)
+ compare_mupdf(tmpdir, gif_palette8_img, gif_palette8_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_gif_animation(tmp_path_factory, gif_animation_img, gif_animation_pdf):
+ tmpdir = tmp_path_factory.mktemp("gif_animation")
+ subprocess.check_call(
+ ["pdfseparate", str(gif_animation_pdf), str(tmpdir / "page-%d.pdf")]
+ )
+ for page in [1, 2]:
+ gif_animation_pdf_nr = tmpdir / ("page-%d.pdf" % page)
+ compare_ghostscript(
+ tmpdir, str(gif_animation_img) + "[%d]" % (page - 1), gif_animation_pdf_nr
+ )
+ compare_poppler(
+ tmpdir, str(gif_animation_img) + "[%d]" % (page - 1), gif_animation_pdf_nr
+ )
+ compare_mupdf(
+ tmpdir, str(gif_animation_img) + "[%d]" % (page - 1), gif_animation_pdf_nr
+ )
+ # pdfimages cannot export palette based images
+ gif_animation_pdf_nr.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["darwin", "win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_tiff_float(tmp_path_factory, tiff_float_img, engine):
+ out_pdf = tmp_path_factory.mktemp("tiff_float") / "out.pdf"
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(tiff_float_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_cmyk8(tmp_path_factory, tiff_cmyk8_img, tiff_cmyk8_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_cmyk8")
+ compare_ghostscript(
+ tmpdir,
+ tiff_cmyk8_img,
+ tiff_cmyk8_pdf,
+ gsdevice="tiff32nc",
+ )
+ # not testing with poppler as it cannot write CMYK images
+ compare_mupdf(tmpdir, tiff_cmyk8_img, tiff_cmyk8_pdf, cmyk=True)
+ compare_pdfimages_tiff(tmpdir, tiff_cmyk8_img, tiff_cmyk8_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_tiff_cmyk16(tmp_path_factory, tiff_cmyk16_img, engine):
+ out_pdf = tmp_path_factory.mktemp("tiff_cmyk16") / "out.pdf"
+ # PIL is unable to read 16 bit CMYK images
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(tiff_cmyk16_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_rgb8(tmp_path_factory, tiff_rgb8_img, tiff_rgb8_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_rgb8")
+ compare_ghostscript(tmpdir, tiff_rgb8_img, tiff_rgb8_pdf, gsdevice="tiff24nc")
+ compare_poppler(tmpdir, tiff_rgb8_img, tiff_rgb8_pdf)
+ compare_mupdf(tmpdir, tiff_rgb8_img, tiff_rgb8_pdf)
+ compare_pdfimages_tiff(tmpdir, tiff_rgb8_img, tiff_rgb8_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_tiff_rgb12(tmp_path_factory, tiff_rgb12_img, engine):
+ out_pdf = tmp_path_factory.mktemp("tiff_rgb12") / "out.pdf"
+ # PIL is unable to preserve more than 8 bits per sample
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(tiff_rgb12_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_tiff_rgb14(tmp_path_factory, tiff_rgb14_img, engine):
+ out_pdf = tmp_path_factory.mktemp("tiff_rgb14") / "out.pdf"
+ # PIL is unable to preserve more than 8 bits per sample
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(tiff_rgb14_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_tiff_rgb16(tmp_path_factory, tiff_rgb16_img, engine):
+ out_pdf = tmp_path_factory.mktemp("tiff_rgb16") / "out.pdf"
+ # PIL is unable to preserve more than 8 bits per sample
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(tiff_rgb16_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_tiff_rgba8(tmp_path_factory, tiff_rgba8_img, engine):
+ out_pdf = tmp_path_factory.mktemp("tiff_rgba8") / "out.pdf"
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(tiff_rgba8_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_tiff_rgba16(tmp_path_factory, tiff_rgba16_img, engine):
+ out_pdf = tmp_path_factory.mktemp("tiff_rgba16") / "out.pdf"
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(tiff_rgba16_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_gray1(tmp_path_factory, tiff_gray1_img, tiff_gray1_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_gray1")
+ compare_ghostscript(tmpdir, tiff_gray1_img, tiff_gray1_pdf, gsdevice="pnggray")
+ compare_poppler(tmpdir, tiff_gray1_img, tiff_gray1_pdf)
+ compare_mupdf(tmpdir, tiff_gray1_img, tiff_gray1_pdf)
+ compare_pdfimages_tiff(tmpdir, tiff_gray1_img, tiff_gray1_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_gray2(tmp_path_factory, tiff_gray2_img, tiff_gray2_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_gray2")
+ compare_ghostscript(tmpdir, tiff_gray2_img, tiff_gray2_pdf, gsdevice="pnggray")
+ compare_poppler(tmpdir, tiff_gray2_img, tiff_gray2_pdf)
+ compare_mupdf(tmpdir, tiff_gray2_img, tiff_gray2_pdf)
+ compare_pdfimages_tiff(tmpdir, tiff_gray2_img, tiff_gray2_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_gray4(tmp_path_factory, tiff_gray4_img, tiff_gray4_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_gray4")
+ compare_ghostscript(tmpdir, tiff_gray4_img, tiff_gray4_pdf, gsdevice="pnggray")
+ compare_poppler(tmpdir, tiff_gray4_img, tiff_gray4_pdf)
+ compare_mupdf(tmpdir, tiff_gray4_img, tiff_gray4_pdf)
+ compare_pdfimages_tiff(tmpdir, tiff_gray4_img, tiff_gray4_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_gray8(tmp_path_factory, tiff_gray8_img, tiff_gray8_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_gray8")
+ compare_ghostscript(tmpdir, tiff_gray8_img, tiff_gray8_pdf, gsdevice="pnggray")
+ compare_poppler(tmpdir, tiff_gray8_img, tiff_gray8_pdf)
+ compare_mupdf(tmpdir, tiff_gray8_img, tiff_gray8_pdf)
+ compare_pdfimages_tiff(tmpdir, tiff_gray8_img, tiff_gray8_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_tiff_gray16(tmp_path_factory, tiff_gray16_img, engine):
+ out_pdf = tmp_path_factory.mktemp("tiff_gray16") / "out.pdf"
+ assert (
+ 0
+ != subprocess.run(
+ [
+ img2pdfprog,
+ "--producer=",
+ "--nodate",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(tiff_gray16_img),
+ ]
+ ).returncode
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_multipage(tmp_path_factory, tiff_multipage_img, tiff_multipage_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_multipage")
+ subprocess.check_call(
+ ["pdfseparate", str(tiff_multipage_pdf), str(tmpdir / "page-%d.pdf")]
+ )
+ for page in [1, 2]:
+ tiff_multipage_pdf_nr = tmpdir / ("page-%d.pdf" % page)
+ compare_ghostscript(
+ tmpdir, str(tiff_multipage_img) + "[%d]" % (page - 1), tiff_multipage_pdf_nr
+ )
+ compare_poppler(
+ tmpdir, str(tiff_multipage_img) + "[%d]" % (page - 1), tiff_multipage_pdf_nr
+ )
+ compare_mupdf(
+ tmpdir, str(tiff_multipage_img) + "[%d]" % (page - 1), tiff_multipage_pdf_nr
+ )
+ compare_pdfimages_tiff(
+ tmpdir, str(tiff_multipage_img) + "[%d]" % (page - 1), tiff_multipage_pdf_nr
+ )
+ tiff_multipage_pdf_nr.unlink()
+
+
+@pytest.mark.skipif(
+ not HAVE_IMAGEMAGICK_MODERN,
+ reason="requires imagemagick with support for keeping the palette depth",
+)
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_palette1(tmp_path_factory, tiff_palette1_img, tiff_palette1_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_palette1")
+ compare_ghostscript(tmpdir, tiff_palette1_img, tiff_palette1_pdf)
+ compare_poppler(tmpdir, tiff_palette1_img, tiff_palette1_pdf)
+ compare_mupdf(tmpdir, tiff_palette1_img, tiff_palette1_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ not HAVE_IMAGEMAGICK_MODERN,
+ reason="requires imagemagick with support for keeping the palette depth",
+)
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_palette2(tmp_path_factory, tiff_palette2_img, tiff_palette2_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_palette2")
+ compare_ghostscript(tmpdir, tiff_palette2_img, tiff_palette2_pdf)
+ compare_poppler(tmpdir, tiff_palette2_img, tiff_palette2_pdf)
+ compare_mupdf(tmpdir, tiff_palette2_img, tiff_palette2_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ not HAVE_IMAGEMAGICK_MODERN,
+ reason="requires imagemagick with support for keeping the palette depth",
+)
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_palette4(tmp_path_factory, tiff_palette4_img, tiff_palette4_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_palette4")
+ compare_ghostscript(tmpdir, tiff_palette4_img, tiff_palette4_pdf)
+ compare_poppler(tmpdir, tiff_palette4_img, tiff_palette4_pdf)
+ compare_mupdf(tmpdir, tiff_palette4_img, tiff_palette4_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_palette8(tmp_path_factory, tiff_palette8_img, tiff_palette8_pdf):
+ tmpdir = tmp_path_factory.mktemp("tiff_palette8")
+ compare_ghostscript(tmpdir, tiff_palette8_img, tiff_palette8_pdf)
+ compare_poppler(tmpdir, tiff_palette8_img, tiff_palette8_pdf)
+ compare_mupdf(tmpdir, tiff_palette8_img, tiff_palette8_pdf)
+ # pdfimages cannot export palette based images
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_ccitt_lsb_m2l_white(
+ tmp_path_factory, tiff_ccitt_lsb_m2l_white_img, tiff_ccitt_lsb_m2l_white_pdf
+):
+ tmpdir = tmp_path_factory.mktemp("tiff_ccitt_lsb_m2l_white")
+ compare_ghostscript(
+ tmpdir,
+ tiff_ccitt_lsb_m2l_white_img,
+ tiff_ccitt_lsb_m2l_white_pdf,
+ gsdevice="pnggray",
+ )
+ compare_poppler(tmpdir, tiff_ccitt_lsb_m2l_white_img, tiff_ccitt_lsb_m2l_white_pdf)
+ compare_mupdf(tmpdir, tiff_ccitt_lsb_m2l_white_img, tiff_ccitt_lsb_m2l_white_pdf)
+ compare_pdfimages_tiff(
+ tmpdir, tiff_ccitt_lsb_m2l_white_img, tiff_ccitt_lsb_m2l_white_pdf
+ )
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_ccitt_msb_m2l_white(
+ tmp_path_factory, tiff_ccitt_msb_m2l_white_img, tiff_ccitt_msb_m2l_white_pdf
+):
+ tmpdir = tmp_path_factory.mktemp("tiff_ccitt_msb_m2l_white")
+ compare_ghostscript(
+ tmpdir,
+ tiff_ccitt_msb_m2l_white_img,
+ tiff_ccitt_msb_m2l_white_pdf,
+ gsdevice="pnggray",
+ )
+ compare_poppler(tmpdir, tiff_ccitt_msb_m2l_white_img, tiff_ccitt_msb_m2l_white_pdf)
+ compare_mupdf(tmpdir, tiff_ccitt_msb_m2l_white_img, tiff_ccitt_msb_m2l_white_pdf)
+ compare_pdfimages_tiff(
+ tmpdir, tiff_ccitt_msb_m2l_white_img, tiff_ccitt_msb_m2l_white_pdf
+ )
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_ccitt_msb_l2m_white(
+ tmp_path_factory, tiff_ccitt_msb_l2m_white_img, tiff_ccitt_msb_l2m_white_pdf
+):
+ tmpdir = tmp_path_factory.mktemp("tiff_ccitt_msb_l2m_white")
+ compare_ghostscript(
+ tmpdir,
+ tiff_ccitt_msb_l2m_white_img,
+ tiff_ccitt_msb_l2m_white_pdf,
+ gsdevice="pnggray",
+ )
+ compare_poppler(tmpdir, tiff_ccitt_msb_l2m_white_img, tiff_ccitt_msb_l2m_white_pdf)
+ compare_mupdf(tmpdir, tiff_ccitt_msb_l2m_white_img, tiff_ccitt_msb_l2m_white_pdf)
+ compare_pdfimages_tiff(
+ tmpdir, tiff_ccitt_msb_l2m_white_img, tiff_ccitt_msb_l2m_white_pdf
+ )
+
+
+@pytest.mark.skipif(
+ not HAVE_IMAGEMAGICK_MODERN,
+ reason="requires imagemagick with support for min-is-black",
+)
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_ccitt_lsb_m2l_black(
+ tmp_path_factory, tiff_ccitt_lsb_m2l_black_img, tiff_ccitt_lsb_m2l_black_pdf
+):
+ tmpdir = tmp_path_factory.mktemp("tiff_ccitt_lsb_m2l_black")
+ compare_ghostscript(
+ tmpdir,
+ tiff_ccitt_lsb_m2l_black_img,
+ tiff_ccitt_lsb_m2l_black_pdf,
+ gsdevice="pnggray",
+ )
+ compare_poppler(tmpdir, tiff_ccitt_lsb_m2l_black_img, tiff_ccitt_lsb_m2l_black_pdf)
+ compare_mupdf(tmpdir, tiff_ccitt_lsb_m2l_black_img, tiff_ccitt_lsb_m2l_black_pdf)
+ compare_pdfimages_tiff(
+ tmpdir, tiff_ccitt_lsb_m2l_black_img, tiff_ccitt_lsb_m2l_black_pdf
+ )
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_ccitt_nometa1(
+ tmp_path_factory, tiff_ccitt_nometa1_img, tiff_ccitt_nometa1_pdf
+):
+ tmpdir = tmp_path_factory.mktemp("tiff_ccitt_nometa1")
+ compare_ghostscript(
+ tmpdir, tiff_ccitt_nometa1_img, tiff_ccitt_nometa1_pdf, gsdevice="pnggray"
+ )
+ compare_poppler(tmpdir, tiff_ccitt_nometa1_img, tiff_ccitt_nometa1_pdf)
+ compare_mupdf(tmpdir, tiff_ccitt_nometa1_img, tiff_ccitt_nometa1_pdf)
+ compare_pdfimages_tiff(tmpdir, tiff_ccitt_nometa1_img, tiff_ccitt_nometa1_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_tiff_ccitt_nometa2(
+ tmp_path_factory, tiff_ccitt_nometa2_img, tiff_ccitt_nometa2_pdf
+):
+ tmpdir = tmp_path_factory.mktemp("tiff_ccitt_nometa2")
+ compare_ghostscript(
+ tmpdir, tiff_ccitt_nometa2_img, tiff_ccitt_nometa2_pdf, gsdevice="pnggray"
+ )
+ compare_poppler(tmpdir, tiff_ccitt_nometa2_img, tiff_ccitt_nometa2_pdf)
+ compare_mupdf(tmpdir, tiff_ccitt_nometa2_img, tiff_ccitt_nometa2_pdf)
+ compare_pdfimages_tiff(tmpdir, tiff_ccitt_nometa2_img, tiff_ccitt_nometa2_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_miff_cmyk8(tmp_path_factory, miff_cmyk8_img, tiff_cmyk8_img, miff_cmyk8_pdf):
+ tmpdir = tmp_path_factory.mktemp("miff_cmyk8")
+ compare_ghostscript(tmpdir, tiff_cmyk8_img, miff_cmyk8_pdf, gsdevice="tiff32nc")
+ # not testing with poppler as it cannot write CMYK images
+ compare_mupdf(tmpdir, tiff_cmyk8_img, miff_cmyk8_pdf, cmyk=True)
+ compare_pdfimages_tiff(tmpdir, tiff_cmyk8_img, miff_cmyk8_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_miff_cmyk16(
+ tmp_path_factory, miff_cmyk16_img, tiff_cmyk16_img, miff_cmyk16_pdf
+):
+ tmpdir = tmp_path_factory.mktemp("miff_cmyk16")
+ compare_ghostscript(
+ tmpdir, tiff_cmyk16_img, miff_cmyk16_pdf, gsdevice="tiff32nc", exact=False
+ )
+ # not testing with poppler as it cannot write CMYK images
+ compare_mupdf(tmpdir, tiff_cmyk16_img, miff_cmyk16_pdf, exact=False, cmyk=True)
+ # compare_pdfimages_tiff(tmpdir, tiff_cmyk16_img, miff_cmyk16_pdf)
+
+
+@pytest.mark.skipif(
+ sys.platform in ["win32"],
+ reason="test utilities not available on Windows and MacOS",
+)
+def test_miff_rgb8(tmp_path_factory, miff_rgb8_img, tiff_rgb8_img, miff_rgb8_pdf):
+ tmpdir = tmp_path_factory.mktemp("miff_rgb8")
+ compare_ghostscript(tmpdir, tiff_rgb8_img, miff_rgb8_pdf, gsdevice="tiff24nc")
+ compare_poppler(tmpdir, tiff_rgb8_img, miff_rgb8_pdf)
+ compare_mupdf(tmpdir, tiff_rgb8_img, miff_rgb8_pdf)
+ compare_pdfimages_tiff(tmpdir, tiff_rgb8_img, miff_rgb8_pdf)
+
+
+# we define some variables so that the table below can be narrower
+psl = (972, 504) # --pagesize landscape
+psp = (504, 972) # --pagesize portrait
+isl = (756, 324) # --imgsize landscape
+isp = (324, 756) # --imgsize portrait
+border = (162, 270) # --border
+poster = (97200, 50400)
+# shortcuts for fit modes
+f_into = img2pdf.FitMode.into
+f_fill = img2pdf.FitMode.fill
+f_exact = img2pdf.FitMode.exact
+f_shrink = img2pdf.FitMode.shrink
+f_enlarge = img2pdf.FitMode.enlarge
+
+
+@pytest.mark.parametrize(
+ "layout_test_cases",
+ [
+ # fmt: off
+ # psp=972x504, psl=504x972, isl=756x324, isp=324x756, border=162:270
+ # --pagesize --border -a pagepdf imgpdf
+ # --imgsize --fit
+ (None, None, None, f_into, 0, (648, 216), (648, 216), # 000
+ (864, 432), (864, 432)),
+ (None, None, None, f_into, 1, (648, 216), (648, 216), # 001
+ (864, 432), (864, 432)),
+ (None, None, None, f_fill, 0, (648, 216), (648, 216), # 002
+ (864, 432), (864, 432)),
+ (None, None, None, f_fill, 1, (648, 216), (648, 216), # 003
+ (864, 432), (864, 432)),
+ (None, None, None, f_exact, 0, (648, 216), (648, 216), # 004
+ (864, 432), (864, 432)),
+ (None, None, None, f_exact, 1, (648, 216), (648, 216), # 005
+ (864, 432), (864, 432)),
+ (None, None, None, f_shrink, 0, (648, 216), (648, 216), # 006
+ (864, 432), (864, 432)),
+ (None, None, None, f_shrink, 1, (648, 216), (648, 216), # 007
+ (864, 432), (864, 432)),
+ (None, None, None, f_enlarge, 0, (648, 216), (648, 216), # 008
+ (864, 432), (864, 432)),
+ (None, None, None, f_enlarge, 1, (648, 216), (648, 216), # 009
+ (864, 432), (864, 432)),
+ (None, None, border, f_into, 0, (1188, 540), (648, 216), # 010
+ (1404, 756), (864, 432)),
+ (None, None, border, f_into, 1, (1188, 540), (648, 216), # 011
+ (1404, 756), (864, 432)),
+ (None, None, border, f_fill, 0, (1188, 540), (648, 216), # 012
+ (1404, 756), (864, 432)),
+ (None, None, border, f_fill, 1, (1188, 540), (648, 216), # 013
+ (1404, 756), (864, 432)),
+ (None, None, border, f_exact, 0, (1188, 540), (648, 216), # 014
+ (1404, 756), (864, 432)),
+ (None, None, border, f_exact, 1, (1188, 540), (648, 216), # 015
+ (1404, 756), (864, 432)),
+ (None, None, border, f_shrink, 0, (1188, 540), (648, 216), # 016
+ (1404, 756), (864, 432)),
+ (None, None, border, f_shrink, 1, (1188, 540), (648, 216), # 017
+ (1404, 756), (864, 432)),
+ (None, None, border, f_enlarge, 0, (1188, 540), (648, 216), # 018
+ (1404, 756), (864, 432)),
+ (None, None, border, f_enlarge, 1, (1188, 540), (648, 216), # 019
+ (1404, 756), (864, 432)),
+ (None, isp, None, f_into, 0, (324, 108), (324, 108), # 020
+ (324, 162), (324, 162)),
+ (None, isp, None, f_into, 1, (324, 108), (324, 108), # 021
+ (324, 162), (324, 162)),
+ (None, isp, None, f_fill, 0, (2268, 756), (2268, 756), # 022
+ (1512, 756), (1512, 756)),
+ (None, isp, None, f_fill, 1, (2268, 756), (2268, 756), # 023
+ (1512, 756), (1512, 756)),
+ (None, isp, None, f_exact, 0, (324, 756), (324, 756), # 024
+ (324, 756), (324, 756)),
+ (None, isp, None, f_exact, 1, (324, 756), (324, 756), # 025
+ (324, 756), (324, 756)),
+ (None, isp, None, f_shrink, 0, (324, 108), (324, 108), # 026
+ (324, 162), (324, 162)),
+ (None, isp, None, f_shrink, 1, (324, 108), (324, 108), # 027
+ (324, 162), (324, 162)),
+ (None, isp, None, f_enlarge, 0, (648, 216), (648, 216), # 028
+ (864, 432), (864, 432)),
+ (None, isp, None, f_enlarge, 1, (648, 216), (648, 216), # 029
+ (864, 432), (864, 432)),
+ (None, isp, border, f_into, 0, (864, 432), (324, 108), # 030
+ (864, 486), (324, 162)),
+ (None, isp, border, f_into, 1, (864, 432), (324, 108), # 031
+ (864, 486), (324, 162)),
+ (None, isp, border, f_fill, 0, (2808, 1080), (2268, 756), # 032
+ (2052, 1080), (1512, 756)),
+ (None, isp, border, f_fill, 1, (2808, 1080), (2268, 756), # 033
+ (2052, 1080), (1512, 756)),
+ (None, isp, border, f_exact, 0, (864, 1080), (324, 756), # 034
+ (864, 1080), (324, 756)),
+ (None, isp, border, f_exact, 1, (864, 1080), (324, 756), # 035
+ (864, 1080), (324, 756)),
+ (None, isp, border, f_shrink, 0, (864, 432), (324, 108), # 036
+ (864, 486), (324, 162)),
+ (None, isp, border, f_shrink, 1, (864, 432), (324, 108), # 037
+ (864, 486), (324, 162)),
+ (None, isp, border, f_enlarge, 0, (1188, 540), (648, 216), # 038
+ (1404, 756), (864, 432)),
+ (None, isp, border, f_enlarge, 1, (1188, 540), (648, 216), # 039
+ (1404, 756), (864, 432)),
+ (None, isl, None, f_into, 0, (756, 252), (756, 252), # 040
+ (648, 324), (648, 324)),
+ (None, isl, None, f_into, 1, (756, 252), (756, 252), # 041
+ (648, 324), (648, 324)),
+ (None, isl, None, f_fill, 0, (972, 324), (972, 324), # 042
+ (756, 378), (756, 378)),
+ (None, isl, None, f_fill, 1, (972, 324), (972, 324), # 043
+ (756, 378), (756, 378)),
+ (None, isl, None, f_exact, 0, (756, 324), (756, 324), # 044
+ (756, 324), (756, 324)),
+ (None, isl, None, f_exact, 1, (756, 324), (756, 324), # 045
+ (756, 324), (756, 324)),
+ (None, isl, None, f_shrink, 0, (648, 216), (648, 216), # 046
+ (648, 324), (648, 324)),
+ (None, isl, None, f_shrink, 1, (648, 216), (648, 216), # 047
+ (648, 324), (648, 324)),
+ (None, isl, None, f_enlarge, 0, (756, 252), (756, 252), # 048
+ (864, 432), (864, 432)),
+ (None, isl, None, f_enlarge, 1, (756, 252), (756, 252), # 049
+ (864, 432), (864, 432)),
+ # psp=972x504, psp=504x972, isl=756x324, isp=324x756, border=162:270
+ # --pagesize --border -a pagepdf imgpdf
+ # --imgsize --fit imgpx
+ (None, isl, border, f_into, 0, (1296, 576), (756, 252), # 050
+ (1188, 648), (648, 324)),
+ (None, isl, border, f_into, 1, (1296, 576), (756, 252), # 051
+ (1188, 648), (648, 324)),
+ (None, isl, border, f_fill, 0, (1512, 648), (972, 324), # 052
+ (1296, 702), (756, 378)),
+ (None, isl, border, f_fill, 1, (1512, 648), (972, 324), # 053
+ (1296, 702), (756, 378)),
+ (None, isl, border, f_exact, 0, (1296, 648), (756, 324), # 054
+ (1296, 648), (756, 324)),
+ (None, isl, border, f_exact, 1, (1296, 648), (756, 324), # 055
+ (1296, 648), (756, 324)),
+ (None, isl, border, f_shrink, 0, (1188, 540), (648, 216), # 056
+ (1188, 648), (648, 324)),
+ (None, isl, border, f_shrink, 1, (1188, 540), (648, 216), # 057
+ (1188, 648), (648, 324)),
+ (None, isl, border, f_enlarge, 0, (1296, 576), (756, 252), # 058
+ (1404, 756), (864, 432)),
+ (None, isl, border, f_enlarge, 1, (1296, 576), (756, 252), # 059
+ (1404, 756), (864, 432)),
+ (psp, None, None, f_into, 0, (504, 972), (504, 168), # 060
+ (504, 972), (504, 252)),
+ (psp, None, None, f_into, 1, (972, 504), (972, 324), # 061
+ (972, 504), (972, 486)),
+ (psp, None, None, f_fill, 0, (504, 972), (2916, 972), # 062
+ (504, 972), (1944, 972)),
+ (psp, None, None, f_fill, 1, (972, 504), (1512, 504), # 063
+ (972, 504), (1008, 504)),
+ (psp, None, None, f_exact, 0, (504, 972), (504, 972), # 064
+ (504, 972), (504, 972)),
+ (psp, None, None, f_exact, 1, (972, 504), (972, 504), # 065
+ (972, 504), (972, 504)),
+ (psp, None, None, f_shrink, 0, (504, 972), (504, 168), # 066
+ (504, 972), (504, 252)),
+ (psp, None, None, f_shrink, 1, (972, 504), (648, 216), # 067
+ (972, 504), (864, 432)),
+ (psp, None, None, f_enlarge, 0, (504, 972), (648, 216), # 068
+ (504, 972), (864, 432)),
+ (psp, None, None, f_enlarge, 1, (972, 504), (972, 324), # 069
+ (972, 504), (972, 486)),
+ (psp, None, border, f_into, 0, None, None, None, None), # 070
+ (psp, None, border, f_into, 1, None, None, None, None), # 071
+ (psp, None, border, f_fill, 0, (504, 972), (1944, 648), # 072
+ (504, 972), (1296, 648)),
+ (psp, None, border, f_fill, 1, (972, 504), (648, 216), # 073
+ (972, 504), (648, 324)),
+ (psp, None, border, f_exact, 0, None, None, None, None), # 074
+ (psp, None, border, f_exact, 1, None, None, None, None), # 075
+ (psp, None, border, f_shrink, 0, None, None, None, None), # 076
+ (psp, None, border, f_shrink, 1, None, None, None, None), # 077
+ (psp, None, border, f_enlarge, 0, (504, 972), (648, 216), # 078
+ (504, 972), (864, 432)),
+ (psp, None, border, f_enlarge, 1, (972, 504), (648, 216), # 079
+ (972, 504), (864, 432)),
+ (psp, isp, None, f_into, 0, (504, 972), (324, 108), # 080
+ (504, 972), (324, 162)),
+ (psp, isp, None, f_into, 1, (972, 504), (324, 108), # 081
+ (972, 504), (324, 162)),
+ (psp, isp, None, f_fill, 0, (504, 972), (2268, 756), # 082
+ (504, 972), (1512, 756)),
+ (psp, isp, None, f_fill, 1, (972, 504), (2268, 756), # 083
+ (972, 504), (1512, 756)),
+ (psp, isp, None, f_exact, 0, (504, 972), (324, 756), # 084
+ (504, 972), (324, 756)),
+ (psp, isp, None, f_exact, 1, (972, 504), (324, 756), # 085
+ (972, 504), (324, 756)),
+ (psp, isp, None, f_shrink, 0, (504, 972), (324, 108), # 086
+ (504, 972), (324, 162)),
+ (psp, isp, None, f_shrink, 1, (972, 504), (324, 108), # 087
+ (972, 504), (324, 162)),
+ (psp, isp, None, f_enlarge, 0, (504, 972), (648, 216), # 088
+ (504, 972), (864, 432)),
+ (psp, isp, None, f_enlarge, 1, (972, 504), (648, 216), # 089
+ (972, 504), (864, 432)),
+ (psp, isp, border, f_into, 0, (504, 972), (324, 108), # 090
+ (504, 972), (324, 162)),
+ (psp, isp, border, f_into, 1, (972, 504), (324, 108), # 091
+ (972, 504), (324, 162)),
+ (psp, isp, border, f_fill, 0, (504, 972), (2268, 756), # 092
+ (504, 972), (1512, 756)),
+ (psp, isp, border, f_fill, 1, (972, 504), (2268, 756), # 093
+ (972, 504), (1512, 756)),
+ (psp, isp, border, f_exact, 0, (504, 972), (324, 756), # 094
+ (504, 972), (324, 756)),
+ (psp, isp, border, f_exact, 1, (972, 504), (324, 756), # 095
+ (972, 504), (324, 756)),
+ (psp, isp, border, f_shrink, 0, (504, 972), (324, 108), # 096
+ (504, 972), (324, 162)),
+ (psp, isp, border, f_shrink, 1, (972, 504), (324, 108), # 097
+ (972, 504), (324, 162)),
+ (psp, isp, border, f_enlarge, 0, (504, 972), (648, 216), # 098
+ (504, 972), (864, 432)),
+ (psp, isp, border, f_enlarge, 1, (972, 504), (648, 216), # 099
+ (972, 504), (864, 432)),
+ # psp=972x504, psp=504x972, isl=756x324, isp=324x756, border=162:270
+ # --pagesize --border -a pagepdf imgpdf
+ # --imgsize --fit imgpx
+ (psp, isl, None, f_into, 0, (504, 972), (756, 252), # 100
+ (504, 972), (648, 324)),
+ (psp, isl, None, f_into, 1, (972, 504), (756, 252), # 101
+ (972, 504), (648, 324)),
+ (psp, isl, None, f_fill, 0, (504, 972), (972, 324), # 102
+ (504, 972), (756, 378)),
+ (psp, isl, None, f_fill, 1, (972, 504), (972, 324), # 103
+ (972, 504), (756, 378)),
+ (psp, isl, None, f_exact, 0, (504, 972), (756, 324), # 104
+ (504, 972), (756, 324)),
+ (psp, isl, None, f_exact, 1, (972, 504), (756, 324), # 105
+ (972, 504), (756, 324)),
+ (psp, isl, None, f_shrink, 0, (504, 972), (648, 216), # 106
+ (504, 972), (648, 324)),
+ (psp, isl, None, f_shrink, 1, (972, 504), (648, 216), # 107
+ (972, 504), (648, 324)),
+ (psp, isl, None, f_enlarge, 0, (504, 972), (756, 252), # 108
+ (504, 972), (864, 432)),
+ (psp, isl, None, f_enlarge, 1, (972, 504), (756, 252), # 109
+ (972, 504), (864, 432)),
+ (psp, isl, border, f_into, 0, (504, 972), (756, 252), # 110
+ (504, 972), (648, 324)),
+ (psp, isl, border, f_into, 1, (972, 504), (756, 252), # 111
+ (972, 504), (648, 324)),
+ (psp, isl, border, f_fill, 0, (504, 972), (972, 324), # 112
+ (504, 972), (756, 378)),
+ (psp, isl, border, f_fill, 1, (972, 504), (972, 324), # 113
+ (972, 504), (756, 378)),
+ (psp, isl, border, f_exact, 0, (504, 972), (756, 324), # 114
+ (504, 972), (756, 324)),
+ (psp, isl, border, f_exact, 1, (972, 504), (756, 324), # 115
+ (972, 504), (756, 324)),
+ (psp, isl, border, f_shrink, 0, (504, 972), (648, 216), # 116
+ (504, 972), (648, 324)),
+ (psp, isl, border, f_shrink, 1, (972, 504), (648, 216), # 117
+ (972, 504), (648, 324)),
+ (psp, isl, border, f_enlarge, 0, (504, 972), (756, 252), # 118
+ (504, 972), (864, 432)),
+ (psp, isl, border, f_enlarge, 1, (972, 504), (756, 252), # 119
+ (972, 504), (864, 432)),
+ (psl, None, None, f_into, 0, (972, 504), (972, 324), # 120
+ (972, 504), (972, 486)),
+ (psl, None, None, f_into, 1, (972, 504), (972, 324), # 121
+ (972, 504), (972, 486)),
+ (psl, None, None, f_fill, 0, (972, 504), (1512, 504), # 122
+ (972, 504), (1008, 504)),
+ (psl, None, None, f_fill, 1, (972, 504), (1512, 504), # 123
+ (972, 504), (1008, 504)),
+ (psl, None, None, f_exact, 0, (972, 504), (972, 504), # 124
+ (972, 504), (972, 504)),
+ (psl, None, None, f_exact, 1, (972, 504), (972, 504), # 125
+ (972, 504), (972, 504)),
+ (psl, None, None, f_shrink, 0, (972, 504), (648, 216), # 126
+ (972, 504), (864, 432)),
+ (psl, None, None, f_shrink, 1, (972, 504), (648, 216), # 127
+ (972, 504), (864, 432)),
+ (psl, None, None, f_enlarge, 0, (972, 504), (972, 324), # 128
+ (972, 504), (972, 486)),
+ (psl, None, None, f_enlarge, 1, (972, 504), (972, 324), # 129
+ (972, 504), (972, 486)),
+ (psl, None, border, f_into, 0, (972, 504), (432, 144), # 130
+ (972, 504), (360, 180)),
+ (psl, None, border, f_into, 1, (972, 504), (432, 144), # 131
+ (972, 504), (360, 180)),
+ (psl, None, border, f_fill, 0, (972, 504), (540, 180), # 132
+ (972, 504), (432, 216)),
+ (psl, None, border, f_fill, 1, (972, 504), (540, 180), # 133
+ (972, 504), (432, 216)),
+ (psl, None, border, f_exact, 0, (972, 504), (432, 180), # 134
+ (972, 504), (432, 180)),
+ (psl, None, border, f_exact, 1, (972, 504), (432, 180), # 135
+ (972, 504), (432, 180)),
+ (psl, None, border, f_shrink, 0, (972, 504), (432, 144), # 136
+ (972, 504), (360, 180)),
+ (psl, None, border, f_shrink, 1, (972, 504), (432, 144), # 137
+ (972, 504), (360, 180)),
+ (psl, None, border, f_enlarge, 0, (972, 504), (648, 216), # 138
+ (972, 504), (864, 432)),
+ (psl, None, border, f_enlarge, 1, (972, 504), (648, 216), # 139
+ (972, 504), (864, 432)),
+ (psl, isp, None, f_into, 0, (972, 504), (324, 108), # 140
+ (972, 504), (324, 162)),
+ (psl, isp, None, f_into, 1, (972, 504), (324, 108), # 141
+ (972, 504), (324, 162)),
+ (psl, isp, None, f_fill, 0, (972, 504), (2268, 756), # 142
+ (972, 504), (1512, 756)),
+ (psl, isp, None, f_fill, 1, (972, 504), (2268, 756), # 143
+ (972, 504), (1512, 756)),
+ (psl, isp, None, f_exact, 0, (972, 504), (324, 756), # 144
+ (972, 504), (324, 756)),
+ (psl, isp, None, f_exact, 1, (972, 504), (324, 756), # 145
+ (972, 504), (324, 756)),
+ (psl, isp, None, f_shrink, 0, (972, 504), (324, 108), # 146
+ (972, 504), (324, 162)),
+ (psl, isp, None, f_shrink, 1, (972, 504), (324, 108), # 147
+ (972, 504), (324, 162)),
+ (psl, isp, None, f_enlarge, 0, (972, 504), (648, 216), # 148
+ (972, 504), (864, 432)),
+ (psl, isp, None, f_enlarge, 1, (972, 504), (648, 216), # 149
+ (972, 504), (864, 432)),
+ # psp=972x504, psl=504x972, isl=756x324, isp=324x756, border=162:270
+ # --pagesize --border -a pagepdf imgpdf
+ # --imgsize --fit imgpx
+ (psl, isp, border, f_into, 0, (972, 504), (324, 108), # 150
+ (972, 504), (324, 162)),
+ (psl, isp, border, f_into, 1, (972, 504), (324, 108), # 151
+ (972, 504), (324, 162)),
+ (psl, isp, border, f_fill, 0, (972, 504), (2268, 756), # 152
+ (972, 504), (1512, 756)),
+ (psl, isp, border, f_fill, 1, (972, 504), (2268, 756), # 153
+ (972, 504), (1512, 756)),
+ (psl, isp, border, f_exact, 0, (972, 504), (324, 756), # 154
+ (972, 504), (324, 756)),
+ (psl, isp, border, f_exact, 1, (972, 504), (324, 756), # 155
+ (972, 504), (324, 756)),
+ (psl, isp, border, f_shrink, 0, (972, 504), (324, 108), # 156
+ (972, 504), (324, 162)),
+ (psl, isp, border, f_shrink, 1, (972, 504), (324, 108), # 157
+ (972, 504), (324, 162)),
+ (psl, isp, border, f_enlarge, 0, (972, 504), (648, 216), # 158
+ (972, 504), (864, 432)),
+ (psl, isp, border, f_enlarge, 1, (972, 504), (648, 216), # 159
+ (972, 504), (864, 432)),
+ (psl, isl, None, f_into, 0, (972, 504), (756, 252), # 160
+ (972, 504), (648, 324)),
+ (psl, isl, None, f_into, 1, (972, 504), (756, 252), # 161
+ (972, 504), (648, 324)),
+ (psl, isl, None, f_fill, 0, (972, 504), (972, 324), # 162
+ (972, 504), (756, 378)),
+ (psl, isl, None, f_fill, 1, (972, 504), (972, 324), # 163
+ (972, 504), (756, 378)),
+ (psl, isl, None, f_exact, 0, (972, 504), (756, 324), # 164
+ (972, 504), (756, 324)),
+ (psl, isl, None, f_exact, 1, (972, 504), (756, 324), # 165
+ (972, 504), (756, 324)),
+ (psl, isl, None, f_shrink, 0, (972, 504), (648, 216), # 166
+ (972, 504), (648, 324)),
+ (psl, isl, None, f_shrink, 1, (972, 504), (648, 216), # 167
+ (972, 504), (648, 324)),
+ (psl, isl, None, f_enlarge, 0, (972, 504), (756, 252), # 168
+ (972, 504), (864, 432)),
+ (psl, isl, None, f_enlarge, 1, (972, 504), (756, 252), # 169
+ (972, 504), (864, 432)),
+ (psl, isl, border, f_into, 0, (972, 504), (756, 252), # 170
+ (972, 504), (648, 324)),
+ (psl, isl, border, f_into, 1, (972, 504), (756, 252), # 171
+ (972, 504), (648, 324)),
+ (psl, isl, border, f_fill, 0, (972, 504), (972, 324), # 172
+ (972, 504), (756, 378)),
+ (psl, isl, border, f_fill, 1, (972, 504), (972, 324), # 173
+ (972, 504), (756, 378)),
+ (psl, isl, border, f_exact, 0, (972, 504), (756, 324), # 174
+ (972, 504), (756, 324)),
+ (psl, isl, border, f_exact, 1, (972, 504), (756, 324), # 175
+ (972, 504), (756, 324)),
+ (psl, isl, border, f_shrink, 0, (972, 504), (648, 216), # 176
+ (972, 504), (648, 324)),
+ (psl, isl, border, f_shrink, 1, (972, 504), (648, 216), # 177
+ (972, 504), (648, 324)),
+ (psl, isl, border, f_enlarge, 0, (972, 504), (756, 252), # 178
+ (972, 504), (864, 432)),
+ (psl, isl, border, f_enlarge, 1, (972, 504), (756, 252), # 179
+ (972, 504), (864, 432)),
+ (poster, None, None, f_fill, 0, (97200, 50400), (151200, 50400),
+ (97200, 50400), (100800, 50400)),
+ ]
+ # fmt: on
+)
+def test_layout(layout_test_cases):
+ # there is no need to have test cases with the same images with inverted
+ # orientation (landscape/portrait) because --pagesize and --imgsize are
+ # already inverted
+ im1 = (864, 288) # imgpx #1 => 648x216
+ im2 = (1152, 576) # imgpx #2 => 864x432
+ psopt, isopt, border, fit, ao, pspdf1, ispdf1, pspdf2, ispdf2 = layout_test_cases
+ if isopt is not None:
+ isopt = ((img2pdf.ImgSize.abs, isopt[0]), (img2pdf.ImgSize.abs, isopt[1]))
+ layout_fun = img2pdf.get_layout_fun(psopt, isopt, border, fit, ao)
+ try:
+ pwpdf, phpdf, iwpdf, ihpdf = layout_fun(
+ im1[0], im1[1], (img2pdf.default_dpi, img2pdf.default_dpi)
+ )
+ assert (pwpdf, phpdf) == pspdf1
+ assert (iwpdf, ihpdf) == ispdf1
+ except img2pdf.NegativeDimensionError:
+ assert pspdf1 is None
+ assert ispdf1 is None
+ try:
+ pwpdf, phpdf, iwpdf, ihpdf = layout_fun(
+ im2[0], im2[1], (img2pdf.default_dpi, img2pdf.default_dpi)
+ )
+ assert (pwpdf, phpdf) == pspdf2
+ assert (iwpdf, ihpdf) == ispdf2
+ except img2pdf.NegativeDimensionError:
+ assert pspdf2 is None
+ assert ispdf2 is None
+
+
+@pytest.fixture(
+ scope="session",
+ params=os.listdir(os.path.join(os.path.dirname(__file__), "tests", "input")),
+)
+def general_input(request):
+ assert os.path.isfile(
+ os.path.join(os.path.dirname(__file__), "tests", "input", request.param)
+ )
+ return request.param
+
+
+@pytest.mark.skipif(not HAVE_FAKETIME, reason="requires faketime")
+@pytest.mark.parametrize(
+ "engine,testdata,timezone,pdfa",
+ itertools.product(
+ ["internal", "pikepdf"],
+ ["2021-02-05 17:49:00"],
+ ["Europe/Berlin", "GMT+12"],
+ [True, False],
+ ),
+)
+def test_faketime(tmp_path_factory, jpg_img, engine, testdata, timezone, pdfa):
+ expected = tz2utcstrftime(testdata, "D:%Y%m%d%H%M%SZ", timezone)
+ out_pdf = tmp_path_factory.mktemp("faketime") / "out.pdf"
+ subprocess.check_call(
+ ["env", f"TZ={timezone}", "faketime", "-f", testdata, img2pdfprog]
+ + (["--pdfa"] if pdfa else [])
+ + [
+ "--producer=",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(jpg_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert p.docinfo.CreationDate == expected
+ assert p.docinfo.ModDate == expected
+ if pdfa:
+ assert p.Root.Metadata.Subtype == "/XML"
+ assert p.Root.Metadata.Type == "/Metadata"
+ expected = tz2utcstrftime(testdata, "%Y-%m-%dT%H:%M:%SZ", timezone)
+ root = ET.fromstring(p.Root.Metadata.read_bytes())
+ for k in ["ModifyDate", "CreateDate"]:
+ assert (
+ root.find(
+ f".//xmp:{k}", {"xmp": "http://ns.adobe.com/xap/1.0/"}
+ ).text
+ == expected
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.parametrize(
+ "engine,testdata,timezone,pdfa",
+ itertools.product(
+ ["internal", "pikepdf"],
+ [
+ "2021-02-05 17:49:00",
+ "2021-02-05T17:49:00",
+ "Fri, 05 Feb 2021 17:49:00 +0100",
+ "last year 12:00",
+ ],
+ ["Europe/Berlin", "GMT+12"],
+ [True, False],
+ ),
+)
+def test_date(tmp_path_factory, jpg_img, engine, testdata, timezone, pdfa):
+ # we use the date utility to convert the timestamp from the local
+ # timezone into UTC with the format used by PDF
+ expected = tz2utcstrftime(testdata, "D:%Y%m%d%H%M%SZ", timezone)
+ out_pdf = tmp_path_factory.mktemp("faketime") / "out.pdf"
+ subprocess.check_call(
+ ["env", f"TZ={timezone}", img2pdfprog]
+ + (["--pdfa"] if pdfa else [])
+ + [
+ f"--moddate={testdata}",
+ f"--creationdate={testdata}",
+ "--producer=",
+ "--engine=" + engine,
+ "--output=" + str(out_pdf),
+ str(jpg_img),
+ ]
+ )
+ with pikepdf.open(str(out_pdf)) as p:
+ assert p.docinfo.CreationDate == expected
+ assert p.docinfo.ModDate == expected
+ if pdfa:
+ assert p.Root.Metadata.Subtype == "/XML"
+ assert p.Root.Metadata.Type == "/Metadata"
+ expected = tz2utcstrftime(testdata, "%Y-%m-%dT%H:%M:%SZ", timezone)
+ root = ET.fromstring(p.Root.Metadata.read_bytes())
+ for k in ["ModifyDate", "CreateDate"]:
+ assert (
+ root.find(
+ f".//xmp:{k}", {"xmp": "http://ns.adobe.com/xap/1.0/"}
+ ).text
+ == expected
+ )
+ out_pdf.unlink()
+
+
+@pytest.mark.parametrize("engine", ["internal", "pikepdf"])
+def test_general(general_input, engine):
+ inputf = os.path.join(os.path.dirname(__file__), "tests", "input", general_input)
+ outputf = os.path.join(
+ os.path.dirname(__file__), "tests", "output", general_input + ".pdf"
+ )
+ assert os.path.isfile(outputf)
+ f = inputf
+ out = outputf
+
+ engine = getattr(img2pdf.Engine, engine)
+
+ with open(f, "rb") as inf:
+ orig_imgdata = inf.read()
+ output = img2pdf.convert(orig_imgdata, nodate=True, engine=engine)
+ x = pikepdf.open(BytesIO(output))
+ assert x.Root.Pages.Count in (1, 2)
+ if len(x.Root.Pages.Kids) == "1":
+ assert x.Size == "7"
+ assert len(x.Root.Pages.Kids) == 1
+ elif len(x.Root.Pages.Kids) == "2":
+ assert x.Size == "10"
+ assert len(x.Root.Pages.Kids) == 2
+ assert sorted(x.Root.keys()) == ["/Pages", "/Type"]
+ assert x.Root.Type == "/Catalog"
+ assert sorted(x.Root.Pages.keys()) == ["/Count", "/Kids", "/Type"]
+ assert x.Root.Pages.Type == "/Pages"
+ orig_img = Image.open(f)
+ for pagenum in range(len(x.Root.Pages.Kids)):
+ # retrieve the original image frame that this page was
+ # generated from
+ orig_img.seek(pagenum)
+ cur_page = x.Root.Pages.Kids[pagenum]
+
+ ndpi = orig_img.info.get("dpi", (96.0, 96.0))
+ # In python3, the returned dpi value for some tiff images will
+ # not be an integer but a float. To make the behaviour of
+ # img2pdf the same between python2 and python3, we convert that
+ # float into an integer by rounding.
+ # Search online for the 72.009 dpi problem for more info.
+ ndpi = (int(round(ndpi[0])), int(round(ndpi[1])))
+ imgwidthpx, imgheightpx = orig_img.size
+ pagewidth = 72.0 * imgwidthpx / ndpi[0]
+ pageheight = 72.0 * imgheightpx / ndpi[1]
+
+ def format_float(f):
+ if int(f) == f:
+ return int(f)
+ else:
+ return decimal.Decimal("%.4f" % f)
+
+ assert sorted(cur_page.keys()) == [
+ "/Contents",
+ "/MediaBox",
+ "/Parent",
+ "/Resources",
+ "/Type",
+ ]
+ assert cur_page.MediaBox == pikepdf.Array(
+ [0, 0, format_float(pagewidth), format_float(pageheight)]
+ )
+ assert cur_page.Parent == x.Root.Pages
+ assert cur_page.Type == "/Page"
+ assert cur_page.Resources.keys() == {"/XObject"}
+ assert cur_page.Resources.XObject.keys() == {"/Im0"}
+ if engine != img2pdf.Engine.pikepdf:
+ assert cur_page.Contents.Length == len(cur_page.Contents.read_bytes())
+ assert (
+ cur_page.Contents.read_bytes()
+ == b"q\n%.4f 0 0 %.4f 0.0000 0.0000 cm\n/Im0 Do\nQ"
+ % (
+ pagewidth,
+ pageheight,
+ )
+ )
+
+ imgprops = cur_page.Resources.XObject.Im0
+
+ # test if the filter is valid:
+ assert imgprops.Filter in [
+ "/DCTDecode",
+ "/JPXDecode",
+ "/FlateDecode",
+ pikepdf.Array([pikepdf.Name.CCITTFaxDecode]),
+ ]
+
+ # test if the image has correct size
+ assert imgprops.Width == orig_img.size[0]
+ assert imgprops.Height == orig_img.size[1]
+ # if the input file is a jpeg then it should've been copied
+ # verbatim into the PDF
+ if imgprops.Filter in ["/DCTDecode", "/JPXDecode"]:
+ assert cur_page.Resources.XObject.Im0.read_raw_bytes() == orig_imgdata
+ elif imgprops.Filter == pikepdf.Array([pikepdf.Name.CCITTFaxDecode]):
+ tiff_header = tiff_header_for_ccitt(
+ int(imgprops.Width), int(imgprops.Height), int(imgprops.Length), 4
+ )
+ imgio = BytesIO()
+ imgio.write(tiff_header)
+ imgio.write(cur_page.Resources.XObject.Im0.read_raw_bytes())
+ imgio.seek(0)
+ im = Image.open(imgio)
+ assert im.tobytes() == orig_img.tobytes()
+ try:
+ im.close()
+ except AttributeError:
+ pass
+ elif imgprops.Filter == "/FlateDecode":
+ # otherwise, the data is flate encoded and has to be equal
+ # to the pixel data of the input image
+ imgdata = zlib.decompress(cur_page.Resources.XObject.Im0.read_raw_bytes())
+ if hasattr(imgprops, "DecodeParms"):
+ if orig_img.format == "PNG":
+ pngidat, palette = img2pdf.parse_png(orig_imgdata)
+ elif (
+ orig_img.format == "TIFF"
+ and orig_img.info["compression"] == "group4"
+ ):
+ offset, length = img2pdf.ccitt_payload_location_from_pil(orig_img)
+ pngidat = orig_imgdata[offset : offset + length]
+ else:
+ pngbuffer = BytesIO()
+ orig_img.save(pngbuffer, format="png")
+ pngidat, palette = img2pdf.parse_png(pngbuffer.getvalue())
+ assert zlib.decompress(pngidat) == imgdata
+ else:
+ colorspace = imgprops.ColorSpace
+ if colorspace == "/DeviceGray":
+ colorspace = "L"
+ elif colorspace == "/DeviceRGB":
+ colorspace = "RGB"
+ elif colorspace == "/DeviceCMYK":
+ colorspace = "CMYK"
+ else:
+ raise Exception("invalid colorspace")
+ im = Image.frombytes(
+ colorspace, (int(imgprops.Width), int(imgprops.Height)), imgdata
+ )
+ if orig_img.mode == "1":
+ assert im.tobytes() == orig_img.convert("L").tobytes()
+ elif orig_img.mode not in ("RGB", "L", "CMYK", "CMYK;I"):
+ assert im.tobytes() == orig_img.convert("RGB").tobytes()
+ # the python-pil version 2.3.0-1ubuntu3 in Ubuntu does
+ # not have the close() method
+ try:
+ im.close()
+ except AttributeError:
+ pass
+ else:
+ raise Exception("unknown filter")
+
+ def rec(obj):
+ if isinstance(obj, pikepdf.Dictionary):
+ return {k: rec(v) for k, v in obj.items() if k != "/Parent"}
+ elif isinstance(obj, pikepdf.Array):
+ return [rec(v) for v in obj]
+ elif isinstance(obj, pikepdf.Stream):
+ ret = rec(obj.stream_dict)
+ stream = obj.read_raw_bytes()
+ assert len(stream) == ret["/Length"]
+ del ret["/Length"]
+ if ret.get("/Filter") == "/FlateDecode":
+ stream = obj.read_bytes()
+ del ret["/Filter"]
+ ret["stream"] = stream
+ return ret
+ elif isinstance(obj, pikepdf.Name) or isinstance(obj, pikepdf.String):
+ return str(obj)
+ elif isinstance(obj, decimal.Decimal) or isinstance(obj, str):
+ return obj
+ elif isinstance(obj, int):
+ return decimal.Decimal(obj)
+ raise Exception("unhandled: %s" % (type(obj)))
+
+ y = pikepdf.open(out)
+ pydictx = rec(x.Root)
+ pydicty = rec(y.Root)
+ assert pydictx == pydicty
+ # the python-pil version 2.3.0-1ubuntu3 in Ubuntu does not have the
+ # close() method
+ try:
+ orig_img.close()
+ except AttributeError:
+ pass
+
+
+def main():
+ normal16 = alpha_value()[:, :, 0:3]
+ pathlib.Path("test.icc").write_bytes(icc_profile())
+ write_png(
+ normal16 / 0xFFFF * 0xFF,
+ "icc.png",
+ 8,
+ 2,
+ iccp="test.icc",
+ )
+ write_png(
+ normal16 / 0xFFFF * 0xFF,
+ "normal.png",
+ 8,
+ 2,
+ )
+
+
+if __name__ == "__main__":
+ main()
diff --git a/src/jp2.py b/src/jp2.py
index 30edb7e..44d3e21 100644
--- a/src/jp2.py
+++ b/src/jp2.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python
#
-# Copyright (C) 2013 Johannes 'josch' Schauer <j.schauer at email.de>
+# Copyright (C) 2013 Johannes Schauer Marin Rodrigues <j.schauer at email.de>
#
# this module is heavily based upon jpylyzer which is
# KB / National Library of the Netherlands, Open Planets Foundation
@@ -23,23 +23,22 @@ import struct
def getBox(data, byteStart, noBytes):
- boxLengthValue = struct.unpack(">I", data[byteStart:byteStart+4])[0]
- boxType = data[byteStart+4:byteStart+8]
+ boxLengthValue = struct.unpack(">I", data[byteStart : byteStart + 4])[0]
+ boxType = data[byteStart + 4 : byteStart + 8]
contentsStartOffset = 8
if boxLengthValue == 1:
- boxLengthValue = struct.unpack(">Q", data[byteStart+8:byteStart+16])[0]
+ boxLengthValue = struct.unpack(">Q", data[byteStart + 8 : byteStart + 16])[0]
contentsStartOffset = 16
if boxLengthValue == 0:
- boxLengthValue = noBytes-byteStart
+ boxLengthValue = noBytes - byteStart
byteEnd = byteStart + boxLengthValue
- boxContents = data[byteStart+contentsStartOffset:byteEnd]
+ boxContents = data[byteStart + contentsStartOffset : byteEnd]
return (boxLengthValue, boxType, byteEnd, boxContents)
def parse_ihdr(data):
- height = struct.unpack(">I", data[0:4])[0]
- width = struct.unpack(">I", data[4:8])[0]
- return width, height
+ height, width, channels, bpp = struct.unpack(">IIHB", data[:11])
+ return width, height, channels, bpp + 1
def parse_colr(data):
@@ -52,14 +51,15 @@ def parse_colr(data):
elif enumCS == 17:
return "L"
else:
- raise Exception("only sRGB and greyscale color space is supported, "
- "got %d" % enumCS)
+ raise Exception(
+ "only sRGB and greyscale color space is supported, " "got %d" % enumCS
+ )
def parse_resc(data):
hnum, hden, vnum, vden, hexp, vexp = struct.unpack(">HHHHBB", data)
- hdpi = ((hnum/hden) * (10**hexp) * 100)/2.54
- vdpi = ((vnum/vden) * (10**vexp) * 100)/2.54
+ hdpi = ((hnum / hden) * (10**hexp) * 100) / 2.54
+ vdpi = ((vnum / vden) * (10**vexp) * 100) / 2.54
return hdpi, vdpi
@@ -69,9 +69,8 @@ def parse_res(data):
byteStart = 0
boxLengthValue = 1 # dummy value for while loop condition
while byteStart < noBytes and boxLengthValue != 0:
- boxLengthValue, boxType, byteEnd, boxContents = \
- getBox(data, byteStart, noBytes)
- if boxType == b'resc':
+ boxLengthValue, boxType, byteEnd, boxContents = getBox(data, byteStart, noBytes)
+ if boxType == b"resc":
hdpi, vdpi = parse_resc(boxContents)
break
return hdpi, vdpi
@@ -83,16 +82,15 @@ def parse_jp2h(data):
byteStart = 0
boxLengthValue = 1 # dummy value for while loop condition
while byteStart < noBytes and boxLengthValue != 0:
- boxLengthValue, boxType, byteEnd, boxContents = \
- getBox(data, byteStart, noBytes)
- if boxType == b'ihdr':
- width, height = parse_ihdr(boxContents)
- elif boxType == b'colr':
+ boxLengthValue, boxType, byteEnd, boxContents = getBox(data, byteStart, noBytes)
+ if boxType == b"ihdr":
+ width, height, channels, bpp = parse_ihdr(boxContents)
+ elif boxType == b"colr":
colorspace = parse_colr(boxContents)
- elif boxType == b'res ':
+ elif boxType == b"res ":
hdpi, vdpi = parse_res(boxContents)
byteStart = byteEnd
- return (width, height, colorspace, hdpi, vdpi)
+ return (width, height, colorspace, hdpi, vdpi, channels, bpp)
def parsejp2(data):
@@ -101,10 +99,11 @@ def parsejp2(data):
boxLengthValue = 1 # dummy value for while loop condition
width, height, colorspace, hdpi, vdpi = None, None, None, None, None
while byteStart < noBytes and boxLengthValue != 0:
- boxLengthValue, boxType, byteEnd, boxContents = \
- getBox(data, byteStart, noBytes)
- if boxType == b'jp2h':
- width, height, colorspace, hdpi, vdpi = parse_jp2h(boxContents)
+ boxLengthValue, boxType, byteEnd, boxContents = getBox(data, byteStart, noBytes)
+ if boxType == b"jp2h":
+ width, height, colorspace, hdpi, vdpi, channels, bpp = parse_jp2h(
+ boxContents
+ )
break
byteStart = byteEnd
if not width:
@@ -114,12 +113,41 @@ def parsejp2(data):
if not colorspace:
raise Exception("no colorspace in jp2 header")
# retrieving the dpi is optional so we do not error out if not present
- return (width, height, colorspace, hdpi, vdpi)
+ return (width, height, colorspace, hdpi, vdpi, channels, bpp)
+
+
+def parsej2k(data):
+ lsiz, rsiz, xsiz, ysiz, xosiz, yosiz, _, _, _, _, csiz = struct.unpack(
+ ">HHIIIIIIIIH", data[4:42]
+ )
+ ssiz = [None] * csiz
+ xrsiz = [None] * csiz
+ yrsiz = [None] * csiz
+ for i in range(csiz):
+ ssiz[i], xrsiz[i], yrsiz[i] = struct.unpack(
+ "BBB", data[42 + 3 * i : 42 + 3 * (i + 1)]
+ )
+ assert ssiz == [7, 7, 7]
+ return xsiz - xosiz, ysiz - yosiz, None, None, None, csiz, 8
+
+
+def parse(data):
+ if data[:4] == b"\xff\x4f\xff\x51":
+ return parsej2k(data)
+ else:
+ return parsejp2(data)
if __name__ == "__main__":
import sys
- width, height, colorspace = parsejp2(open(sys.argv[1]).read())
- sys.stdout.write("width = %d" % width)
- sys.stdout.write("height = %d" % height)
- sys.stdout.write("colorspace = %s" % colorspace)
+
+ width, height, colorspace, hdpi, vdpi, channels, bpp = parse(
+ open(sys.argv[1], "rb").read()
+ )
+ print("width = %d" % width)
+ print("height = %d" % height)
+ print("colorspace = %s" % colorspace)
+ print("hdpi = %s" % hdpi)
+ print("vdpi = %s" % vdpi)
+ print("channels = %s" % channels)
+ print("bpp = %s" % bpp)
diff --git a/src/tests/__init__.py b/src/tests/__init__.py
deleted file mode 100644
index 807aa84..0000000
--- a/src/tests/__init__.py
+++ /dev/null
@@ -1,732 +0,0 @@
-import unittest
-
-import img2pdf
-import os
-import struct
-import sys
-import zlib
-from PIL import Image
-from io import StringIO, BytesIO, TextIOWrapper
-
-HERE = os.path.dirname(__file__)
-
-PY3 = sys.version_info[0] >= 3
-
-if PY3:
- PdfReaderIO = StringIO
-else:
- PdfReaderIO = BytesIO
-
-# Recompressing the image stream makes the comparison robust against output
-# preserving changes in the zlib compress output bitstream
-# (e.g. between different zlib implementations/versions/releases).
-# Without this, some img2pdf 0.3.2 tests fail on Fedora 29/aarch64.
-# See also:
-# https://gitlab.mister-muffin.de/josch/img2pdf/issues/51
-# https://lists.fedoraproject.org/archives/list/devel@lists.fedoraproject.org/thread/R7GD4L5Z6HELCDAL2RDESWR2F3ZXHWVX/
-def recompress_last_stream(bs):
- length_pos = bs.rindex(b'/Length')
- li = length_pos + 8
- lj = bs.index(b' ', li)
- n = int(bs[li:lj])
- stream_pos = bs.index(b'\nstream\n', lj)
- si = stream_pos + 8
- sj = si + n
- startx_pos = bs.rindex(b'\nstartxref\n')
- xi = startx_pos + 11
- xj = bs.index(b'\n', xi)
- m = int(bs[xi:xj])
-
- unc_t = zlib.decompress(bs[si:sj])
- t = zlib.compress(unc_t)
-
- new_len = str(len(t)).encode('ascii')
- u = (lj-li) + n
- v = len(new_len) + len(t)
- off = v - u
-
- rs = (bs[:li] + new_len + bs[lj:si] + t + bs[sj:xi]
- + str(m+off).encode('ascii') + bs[xj:])
-
- return rs
-
-def compare_pdf(outx, outy):
- if b'/FlateDecode' in outx:
- x = recompress_last_stream(outx)
- y = recompress_last_stream(outy)
- if x != y:
- print('original outx:\n{}\nouty:\n{}\n'.format(outx, outy), file=sys.stderr)
- print('recompressed outx:\n{}\nouty:\n{}\n'.format(x, y), file=sys.stderr)
- return False
- else:
- if outx != outy:
- print('original outx:\n{}\nouty:\n{}\n'.format(outx, outy), file=sys.stderr)
- return True
-
-# convert +set date:create +set date:modify -define png:exclude-chunk=time
-
-# we define some variables so that the table below can be narrower
-psl = (972, 504) # --pagesize landscape
-psp = (504, 972) # --pagesize portrait
-isl = (756, 324) # --imgsize landscape
-isp = (324, 756) # --imgsize portrait
-border = (162, 270) # --border
-poster = (97200, 50400)
-# there is no need to have test cases with the same images with inverted
-# orientation (landscape/portrait) because --pagesize and --imgsize are
-# already inverted
-im1 = (864, 288) # imgpx #1 => 648x216
-im2 = (1152, 576) # imgpx #2 => 864x432
-# shortcuts for fit modes
-f_into = img2pdf.FitMode.into
-f_fill = img2pdf.FitMode.fill
-f_exact = img2pdf.FitMode.exact
-f_shrink = img2pdf.FitMode.shrink
-f_enlarge = img2pdf.FitMode.enlarge
-layout_test_cases = [
- # psp=972x504, psl=504x972, isl=756x324, isp=324x756, border=162:270
- # --pagesize --border -a pagepdf imgpdf
- # --imgsize --fit
- (None, None, None, f_into, 0, (648, 216), (648, 216), # 000
- (864, 432), (864, 432)),
- (None, None, None, f_into, 1, (648, 216), (648, 216), # 001
- (864, 432), (864, 432)),
- (None, None, None, f_fill, 0, (648, 216), (648, 216), # 002
- (864, 432), (864, 432)),
- (None, None, None, f_fill, 1, (648, 216), (648, 216), # 003
- (864, 432), (864, 432)),
- (None, None, None, f_exact, 0, (648, 216), (648, 216), # 004
- (864, 432), (864, 432)),
- (None, None, None, f_exact, 1, (648, 216), (648, 216), # 005
- (864, 432), (864, 432)),
- (None, None, None, f_shrink, 0, (648, 216), (648, 216), # 006
- (864, 432), (864, 432)),
- (None, None, None, f_shrink, 1, (648, 216), (648, 216), # 007
- (864, 432), (864, 432)),
- (None, None, None, f_enlarge, 0, (648, 216), (648, 216), # 008
- (864, 432), (864, 432)),
- (None, None, None, f_enlarge, 1, (648, 216), (648, 216), # 009
- (864, 432), (864, 432)),
- (None, None, border, f_into, 0, (1188, 540), (648, 216), # 010
- (1404, 756), (864, 432)),
- (None, None, border, f_into, 1, (1188, 540), (648, 216), # 011
- (1404, 756), (864, 432)),
- (None, None, border, f_fill, 0, (1188, 540), (648, 216), # 012
- (1404, 756), (864, 432)),
- (None, None, border, f_fill, 1, (1188, 540), (648, 216), # 013
- (1404, 756), (864, 432)),
- (None, None, border, f_exact, 0, (1188, 540), (648, 216), # 014
- (1404, 756), (864, 432)),
- (None, None, border, f_exact, 1, (1188, 540), (648, 216), # 015
- (1404, 756), (864, 432)),
- (None, None, border, f_shrink, 0, (1188, 540), (648, 216), # 016
- (1404, 756), (864, 432)),
- (None, None, border, f_shrink, 1, (1188, 540), (648, 216), # 017
- (1404, 756), (864, 432)),
- (None, None, border, f_enlarge, 0, (1188, 540), (648, 216), # 018
- (1404, 756), (864, 432)),
- (None, None, border, f_enlarge, 1, (1188, 540), (648, 216), # 019
- (1404, 756), (864, 432)),
- (None, isp, None, f_into, 0, (324, 108), (324, 108), # 020
- (324, 162), (324, 162)),
- (None, isp, None, f_into, 1, (324, 108), (324, 108), # 021
- (324, 162), (324, 162)),
- (None, isp, None, f_fill, 0, (2268, 756), (2268, 756), # 022
- (1512, 756), (1512, 756)),
- (None, isp, None, f_fill, 1, (2268, 756), (2268, 756), # 023
- (1512, 756), (1512, 756)),
- (None, isp, None, f_exact, 0, (324, 756), (324, 756), # 024
- (324, 756), (324, 756)),
- (None, isp, None, f_exact, 1, (324, 756), (324, 756), # 025
- (324, 756), (324, 756)),
- (None, isp, None, f_shrink, 0, (324, 108), (324, 108), # 026
- (324, 162), (324, 162)),
- (None, isp, None, f_shrink, 1, (324, 108), (324, 108), # 027
- (324, 162), (324, 162)),
- (None, isp, None, f_enlarge, 0, (648, 216), (648, 216), # 028
- (864, 432), (864, 432)),
- (None, isp, None, f_enlarge, 1, (648, 216), (648, 216), # 029
- (864, 432), (864, 432)),
- (None, isp, border, f_into, 0, (864, 432), (324, 108), # 030
- (864, 486), (324, 162)),
- (None, isp, border, f_into, 1, (864, 432), (324, 108), # 031
- (864, 486), (324, 162)),
- (None, isp, border, f_fill, 0, (2808, 1080), (2268, 756), # 032
- (2052, 1080), (1512, 756)),
- (None, isp, border, f_fill, 1, (2808, 1080), (2268, 756), # 033
- (2052, 1080), (1512, 756)),
- (None, isp, border, f_exact, 0, (864, 1080), (324, 756), # 034
- (864, 1080), (324, 756)),
- (None, isp, border, f_exact, 1, (864, 1080), (324, 756), # 035
- (864, 1080), (324, 756)),
- (None, isp, border, f_shrink, 0, (864, 432), (324, 108), # 036
- (864, 486), (324, 162)),
- (None, isp, border, f_shrink, 1, (864, 432), (324, 108), # 037
- (864, 486), (324, 162)),
- (None, isp, border, f_enlarge, 0, (1188, 540), (648, 216), # 038
- (1404, 756), (864, 432)),
- (None, isp, border, f_enlarge, 1, (1188, 540), (648, 216), # 039
- (1404, 756), (864, 432)),
- (None, isl, None, f_into, 0, (756, 252), (756, 252), # 040
- (648, 324), (648, 324)),
- (None, isl, None, f_into, 1, (756, 252), (756, 252), # 041
- (648, 324), (648, 324)),
- (None, isl, None, f_fill, 0, (972, 324), (972, 324), # 042
- (756, 378), (756, 378)),
- (None, isl, None, f_fill, 1, (972, 324), (972, 324), # 043
- (756, 378), (756, 378)),
- (None, isl, None, f_exact, 0, (756, 324), (756, 324), # 044
- (756, 324), (756, 324)),
- (None, isl, None, f_exact, 1, (756, 324), (756, 324), # 045
- (756, 324), (756, 324)),
- (None, isl, None, f_shrink, 0, (648, 216), (648, 216), # 046
- (648, 324), (648, 324)),
- (None, isl, None, f_shrink, 1, (648, 216), (648, 216), # 047
- (648, 324), (648, 324)),
- (None, isl, None, f_enlarge, 0, (756, 252), (756, 252), # 048
- (864, 432), (864, 432)),
- (None, isl, None, f_enlarge, 1, (756, 252), (756, 252), # 049
- (864, 432), (864, 432)),
- # psp=972x504, psp=504x972, isl=756x324, isp=324x756, border=162:270
- # --pagesize --border -a pagepdf imgpdf
- # --imgsize --fit imgpx
- (None, isl, border, f_into, 0, (1296, 576), (756, 252), # 050
- (1188, 648), (648, 324)),
- (None, isl, border, f_into, 1, (1296, 576), (756, 252), # 051
- (1188, 648), (648, 324)),
- (None, isl, border, f_fill, 0, (1512, 648), (972, 324), # 052
- (1296, 702), (756, 378)),
- (None, isl, border, f_fill, 1, (1512, 648), (972, 324), # 053
- (1296, 702), (756, 378)),
- (None, isl, border, f_exact, 0, (1296, 648), (756, 324), # 054
- (1296, 648), (756, 324)),
- (None, isl, border, f_exact, 1, (1296, 648), (756, 324), # 055
- (1296, 648), (756, 324)),
- (None, isl, border, f_shrink, 0, (1188, 540), (648, 216), # 056
- (1188, 648), (648, 324)),
- (None, isl, border, f_shrink, 1, (1188, 540), (648, 216), # 057
- (1188, 648), (648, 324)),
- (None, isl, border, f_enlarge, 0, (1296, 576), (756, 252), # 058
- (1404, 756), (864, 432)),
- (None, isl, border, f_enlarge, 1, (1296, 576), (756, 252), # 059
- (1404, 756), (864, 432)),
- (psp, None, None, f_into, 0, (504, 972), (504, 168), # 060
- (504, 972), (504, 252)),
- (psp, None, None, f_into, 1, (972, 504), (972, 324), # 061
- (972, 504), (972, 486)),
- (psp, None, None, f_fill, 0, (504, 972), (2916, 972), # 062
- (504, 972), (1944, 972)),
- (psp, None, None, f_fill, 1, (972, 504), (1512, 504), # 063
- (972, 504), (1008, 504)),
- (psp, None, None, f_exact, 0, (504, 972), (504, 972), # 064
- (504, 972), (504, 972)),
- (psp, None, None, f_exact, 1, (972, 504), (972, 504), # 065
- (972, 504), (972, 504)),
- (psp, None, None, f_shrink, 0, (504, 972), (504, 168), # 066
- (504, 972), (504, 252)),
- (psp, None, None, f_shrink, 1, (972, 504), (648, 216), # 067
- (972, 504), (864, 432)),
- (psp, None, None, f_enlarge, 0, (504, 972), (648, 216), # 068
- (504, 972), (864, 432)),
- (psp, None, None, f_enlarge, 1, (972, 504), (972, 324), # 069
- (972, 504), (972, 486)),
- (psp, None, border, f_into, 0, None, None, None, None), # 070
- (psp, None, border, f_into, 1, None, None, None, None), # 071
- (psp, None, border, f_fill, 0, (504, 972), (1944, 648), # 072
- (504, 972), (1296, 648)),
- (psp, None, border, f_fill, 1, (972, 504), (648, 216), # 073
- (972, 504), (648, 324)),
- (psp, None, border, f_exact, 0, None, None, None, None), # 074
- (psp, None, border, f_exact, 1, None, None, None, None), # 075
- (psp, None, border, f_shrink, 0, None, None, None, None), # 076
- (psp, None, border, f_shrink, 1, None, None, None, None), # 077
- (psp, None, border, f_enlarge, 0, (504, 972), (648, 216), # 078
- (504, 972), (864, 432)),
- (psp, None, border, f_enlarge, 1, (972, 504), (648, 216), # 079
- (972, 504), (864, 432)),
- (psp, isp, None, f_into, 0, (504, 972), (324, 108), # 080
- (504, 972), (324, 162)),
- (psp, isp, None, f_into, 1, (972, 504), (324, 108), # 081
- (972, 504), (324, 162)),
- (psp, isp, None, f_fill, 0, (504, 972), (2268, 756), # 082
- (504, 972), (1512, 756)),
- (psp, isp, None, f_fill, 1, (972, 504), (2268, 756), # 083
- (972, 504), (1512, 756)),
- (psp, isp, None, f_exact, 0, (504, 972), (324, 756), # 084
- (504, 972), (324, 756)),
- (psp, isp, None, f_exact, 1, (972, 504), (324, 756), # 085
- (972, 504), (324, 756)),
- (psp, isp, None, f_shrink, 0, (504, 972), (324, 108), # 086
- (504, 972), (324, 162)),
- (psp, isp, None, f_shrink, 1, (972, 504), (324, 108), # 087
- (972, 504), (324, 162)),
- (psp, isp, None, f_enlarge, 0, (504, 972), (648, 216), # 088
- (504, 972), (864, 432)),
- (psp, isp, None, f_enlarge, 1, (972, 504), (648, 216), # 089
- (972, 504), (864, 432)),
- (psp, isp, border, f_into, 0, (504, 972), (324, 108), # 090
- (504, 972), (324, 162)),
- (psp, isp, border, f_into, 1, (972, 504), (324, 108), # 091
- (972, 504), (324, 162)),
- (psp, isp, border, f_fill, 0, (504, 972), (2268, 756), # 092
- (504, 972), (1512, 756)),
- (psp, isp, border, f_fill, 1, (972, 504), (2268, 756), # 093
- (972, 504), (1512, 756)),
- (psp, isp, border, f_exact, 0, (504, 972), (324, 756), # 094
- (504, 972), (324, 756)),
- (psp, isp, border, f_exact, 1, (972, 504), (324, 756), # 095
- (972, 504), (324, 756)),
- (psp, isp, border, f_shrink, 0, (504, 972), (324, 108), # 096
- (504, 972), (324, 162)),
- (psp, isp, border, f_shrink, 1, (972, 504), (324, 108), # 097
- (972, 504), (324, 162)),
- (psp, isp, border, f_enlarge, 0, (504, 972), (648, 216), # 098
- (504, 972), (864, 432)),
- (psp, isp, border, f_enlarge, 1, (972, 504), (648, 216), # 099
- (972, 504), (864, 432)),
- # psp=972x504, psp=504x972, isl=756x324, isp=324x756, border=162:270
- # --pagesize --border -a pagepdf imgpdf
- # --imgsize --fit imgpx
- (psp, isl, None, f_into, 0, (504, 972), (756, 252), # 100
- (504, 972), (648, 324)),
- (psp, isl, None, f_into, 1, (972, 504), (756, 252), # 101
- (972, 504), (648, 324)),
- (psp, isl, None, f_fill, 0, (504, 972), (972, 324), # 102
- (504, 972), (756, 378)),
- (psp, isl, None, f_fill, 1, (972, 504), (972, 324), # 103
- (972, 504), (756, 378)),
- (psp, isl, None, f_exact, 0, (504, 972), (756, 324), # 104
- (504, 972), (756, 324)),
- (psp, isl, None, f_exact, 1, (972, 504), (756, 324), # 105
- (972, 504), (756, 324)),
- (psp, isl, None, f_shrink, 0, (504, 972), (648, 216), # 106
- (504, 972), (648, 324)),
- (psp, isl, None, f_shrink, 1, (972, 504), (648, 216), # 107
- (972, 504), (648, 324)),
- (psp, isl, None, f_enlarge, 0, (504, 972), (756, 252), # 108
- (504, 972), (864, 432)),
- (psp, isl, None, f_enlarge, 1, (972, 504), (756, 252), # 109
- (972, 504), (864, 432)),
- (psp, isl, border, f_into, 0, (504, 972), (756, 252), # 110
- (504, 972), (648, 324)),
- (psp, isl, border, f_into, 1, (972, 504), (756, 252), # 111
- (972, 504), (648, 324)),
- (psp, isl, border, f_fill, 0, (504, 972), (972, 324), # 112
- (504, 972), (756, 378)),
- (psp, isl, border, f_fill, 1, (972, 504), (972, 324), # 113
- (972, 504), (756, 378)),
- (psp, isl, border, f_exact, 0, (504, 972), (756, 324), # 114
- (504, 972), (756, 324)),
- (psp, isl, border, f_exact, 1, (972, 504), (756, 324), # 115
- (972, 504), (756, 324)),
- (psp, isl, border, f_shrink, 0, (504, 972), (648, 216), # 116
- (504, 972), (648, 324)),
- (psp, isl, border, f_shrink, 1, (972, 504), (648, 216), # 117
- (972, 504), (648, 324)),
- (psp, isl, border, f_enlarge, 0, (504, 972), (756, 252), # 118
- (504, 972), (864, 432)),
- (psp, isl, border, f_enlarge, 1, (972, 504), (756, 252), # 119
- (972, 504), (864, 432)),
- (psl, None, None, f_into, 0, (972, 504), (972, 324), # 120
- (972, 504), (972, 486)),
- (psl, None, None, f_into, 1, (972, 504), (972, 324), # 121
- (972, 504), (972, 486)),
- (psl, None, None, f_fill, 0, (972, 504), (1512, 504), # 122
- (972, 504), (1008, 504)),
- (psl, None, None, f_fill, 1, (972, 504), (1512, 504), # 123
- (972, 504), (1008, 504)),
- (psl, None, None, f_exact, 0, (972, 504), (972, 504), # 124
- (972, 504), (972, 504)),
- (psl, None, None, f_exact, 1, (972, 504), (972, 504), # 125
- (972, 504), (972, 504)),
- (psl, None, None, f_shrink, 0, (972, 504), (648, 216), # 126
- (972, 504), (864, 432)),
- (psl, None, None, f_shrink, 1, (972, 504), (648, 216), # 127
- (972, 504), (864, 432)),
- (psl, None, None, f_enlarge, 0, (972, 504), (972, 324), # 128
- (972, 504), (972, 486)),
- (psl, None, None, f_enlarge, 1, (972, 504), (972, 324), # 129
- (972, 504), (972, 486)),
- (psl, None, border, f_into, 0, (972, 504), (432, 144), # 130
- (972, 504), (360, 180)),
- (psl, None, border, f_into, 1, (972, 504), (432, 144), # 131
- (972, 504), (360, 180)),
- (psl, None, border, f_fill, 0, (972, 504), (540, 180), # 132
- (972, 504), (432, 216)),
- (psl, None, border, f_fill, 1, (972, 504), (540, 180), # 133
- (972, 504), (432, 216)),
- (psl, None, border, f_exact, 0, (972, 504), (432, 180), # 134
- (972, 504), (432, 180)),
- (psl, None, border, f_exact, 1, (972, 504), (432, 180), # 135
- (972, 504), (432, 180)),
- (psl, None, border, f_shrink, 0, (972, 504), (432, 144), # 136
- (972, 504), (360, 180)),
- (psl, None, border, f_shrink, 1, (972, 504), (432, 144), # 137
- (972, 504), (360, 180)),
- (psl, None, border, f_enlarge, 0, (972, 504), (648, 216), # 138
- (972, 504), (864, 432)),
- (psl, None, border, f_enlarge, 1, (972, 504), (648, 216), # 139
- (972, 504), (864, 432)),
- (psl, isp, None, f_into, 0, (972, 504), (324, 108), # 140
- (972, 504), (324, 162)),
- (psl, isp, None, f_into, 1, (972, 504), (324, 108), # 141
- (972, 504), (324, 162)),
- (psl, isp, None, f_fill, 0, (972, 504), (2268, 756), # 142
- (972, 504), (1512, 756)),
- (psl, isp, None, f_fill, 1, (972, 504), (2268, 756), # 143
- (972, 504), (1512, 756)),
- (psl, isp, None, f_exact, 0, (972, 504), (324, 756), # 144
- (972, 504), (324, 756)),
- (psl, isp, None, f_exact, 1, (972, 504), (324, 756), # 145
- (972, 504), (324, 756)),
- (psl, isp, None, f_shrink, 0, (972, 504), (324, 108), # 146
- (972, 504), (324, 162)),
- (psl, isp, None, f_shrink, 1, (972, 504), (324, 108), # 147
- (972, 504), (324, 162)),
- (psl, isp, None, f_enlarge, 0, (972, 504), (648, 216), # 148
- (972, 504), (864, 432)),
- (psl, isp, None, f_enlarge, 1, (972, 504), (648, 216), # 149
- (972, 504), (864, 432)),
- # psp=972x504, psl=504x972, isl=756x324, isp=324x756, border=162:270
- # --pagesize --border -a pagepdf imgpdf
- # --imgsize --fit imgpx
- (psl, isp, border, f_into, 0, (972, 504), (324, 108), # 150
- (972, 504), (324, 162)),
- (psl, isp, border, f_into, 1, (972, 504), (324, 108), # 151
- (972, 504), (324, 162)),
- (psl, isp, border, f_fill, 0, (972, 504), (2268, 756), # 152
- (972, 504), (1512, 756)),
- (psl, isp, border, f_fill, 1, (972, 504), (2268, 756), # 153
- (972, 504), (1512, 756)),
- (psl, isp, border, f_exact, 0, (972, 504), (324, 756), # 154
- (972, 504), (324, 756)),
- (psl, isp, border, f_exact, 1, (972, 504), (324, 756), # 155
- (972, 504), (324, 756)),
- (psl, isp, border, f_shrink, 0, (972, 504), (324, 108), # 156
- (972, 504), (324, 162)),
- (psl, isp, border, f_shrink, 1, (972, 504), (324, 108), # 157
- (972, 504), (324, 162)),
- (psl, isp, border, f_enlarge, 0, (972, 504), (648, 216), # 158
- (972, 504), (864, 432)),
- (psl, isp, border, f_enlarge, 1, (972, 504), (648, 216), # 159
- (972, 504), (864, 432)),
- (psl, isl, None, f_into, 0, (972, 504), (756, 252), # 160
- (972, 504), (648, 324)),
- (psl, isl, None, f_into, 1, (972, 504), (756, 252), # 161
- (972, 504), (648, 324)),
- (psl, isl, None, f_fill, 0, (972, 504), (972, 324), # 162
- (972, 504), (756, 378)),
- (psl, isl, None, f_fill, 1, (972, 504), (972, 324), # 163
- (972, 504), (756, 378)),
- (psl, isl, None, f_exact, 0, (972, 504), (756, 324), # 164
- (972, 504), (756, 324)),
- (psl, isl, None, f_exact, 1, (972, 504), (756, 324), # 165
- (972, 504), (756, 324)),
- (psl, isl, None, f_shrink, 0, (972, 504), (648, 216), # 166
- (972, 504), (648, 324)),
- (psl, isl, None, f_shrink, 1, (972, 504), (648, 216), # 167
- (972, 504), (648, 324)),
- (psl, isl, None, f_enlarge, 0, (972, 504), (756, 252), # 168
- (972, 504), (864, 432)),
- (psl, isl, None, f_enlarge, 1, (972, 504), (756, 252), # 169
- (972, 504), (864, 432)),
- (psl, isl, border, f_into, 0, (972, 504), (756, 252), # 170
- (972, 504), (648, 324)),
- (psl, isl, border, f_into, 1, (972, 504), (756, 252), # 171
- (972, 504), (648, 324)),
- (psl, isl, border, f_fill, 0, (972, 504), (972, 324), # 172
- (972, 504), (756, 378)),
- (psl, isl, border, f_fill, 1, (972, 504), (972, 324), # 173
- (972, 504), (756, 378)),
- (psl, isl, border, f_exact, 0, (972, 504), (756, 324), # 174
- (972, 504), (756, 324)),
- (psl, isl, border, f_exact, 1, (972, 504), (756, 324), # 175
- (972, 504), (756, 324)),
- (psl, isl, border, f_shrink, 0, (972, 504), (648, 216), # 176
- (972, 504), (648, 324)),
- (psl, isl, border, f_shrink, 1, (972, 504), (648, 216), # 177
- (972, 504), (648, 324)),
- (psl, isl, border, f_enlarge, 0, (972, 504), (756, 252), # 178
- (972, 504), (864, 432)),
- (psl, isl, border, f_enlarge, 1, (972, 504), (756, 252), # 179
- (972, 504), (864, 432)),
- (poster, None, None, f_fill, 0, (97200, 50400), (151200, 50400),
- (97200, 50400), (100800, 50400)),
-]
-
-
-def tiff_header_for_ccitt(width, height, img_size, ccitt_group=4):
- # Quick and dirty TIFF header builder from
- # https://stackoverflow.com/questions/2641770
- tiff_header_struct = '<' + '2s' + 'h' + 'l' + 'h' + 'hhll' * 8 + 'h'
- return struct.pack(
- tiff_header_struct,
- b'II', # Byte order indication: Little indian
- 42, # Version number (always 42)
- 8, # Offset to first IFD
- 8, # Number of tags in IFD
- 256, 4, 1, width, # ImageWidth, LONG, 1, width
- 257, 4, 1, height, # ImageLength, LONG, 1, lenght
- 258, 3, 1, 1, # BitsPerSample, SHORT, 1, 1
- 259, 3, 1, ccitt_group, # Compression, SHORT, 1, 4 = CCITT Group 4
- 262, 3, 1, 1, # Threshholding, SHORT, 1, 0 = WhiteIsZero
- 273, 4, 1, struct.calcsize(
- tiff_header_struct), # StripOffsets, LONG, 1, len of header
- 278, 4, 1, height, # RowsPerStrip, LONG, 1, lenght
- 279, 4, 1, img_size, # StripByteCounts, LONG, 1, size of image
- 0 # last IFD
- )
-
-
-class CommandLineTests(unittest.TestCase):
- def test_main_help(self):
- if PY3:
- from contextlib import redirect_stdout
- f = StringIO()
- with redirect_stdout(f):
- try:
- img2pdf.main(['img2pdf', '--help'])
- except SystemExit:
- pass
- res = f.getvalue()
- self.assertIn('img2pdf', res)
- else:
- # silence output
- sys_stdout = sys.stdout
- sys.stdout = BytesIO()
-
- try:
- img2pdf.main(['img2pdf', '--help'])
- except SystemExit:
- # argparse does sys.exit(0) on --help
- res = sys.stdout.getvalue()
- self.assertIn('img2pdf', res)
- finally:
- sys.stdout = sys_stdout
-
-
-def test_suite():
- class TestImg2Pdf(unittest.TestCase):
- pass
-
- for i, (psopt, isopt, border, fit, ao, pspdf1, ispdf1,
- pspdf2, ispdf2) in enumerate(layout_test_cases):
- if isopt is not None:
- isopt = ((img2pdf.ImgSize.abs, isopt[0]),
- (img2pdf.ImgSize.abs, isopt[1]))
-
- def layout_handler(
- self, psopt, isopt, border, fit, ao, pspdf, ispdf, im):
- layout_fun = img2pdf.get_layout_fun(psopt, isopt, border, fit, ao)
- try:
- pwpdf, phpdf, iwpdf, ihpdf = \
- layout_fun(im[0], im[1], (img2pdf.default_dpi,
- img2pdf.default_dpi))
- self.assertEqual((pwpdf, phpdf), pspdf)
- self.assertEqual((iwpdf, ihpdf), ispdf)
- except img2pdf.NegativeDimensionError:
- self.assertEqual(None, pspdf)
- self.assertEqual(None, ispdf)
-
- def layout_handler_im1(self, psopt=psopt, isopt=isopt, border=border,
- fit=fit, ao=ao, pspdf=pspdf1, ispdf=ispdf1):
- layout_handler(self, psopt, isopt, border, fit, ao, pspdf, ispdf,
- im1)
- setattr(TestImg2Pdf, "test_layout_%03d_im1" % i, layout_handler_im1)
-
- def layout_handler_im2(self, psopt=psopt, isopt=isopt, border=border,
- fit=fit, ao=ao, pspdf=pspdf2, ispdf=ispdf2):
- layout_handler(self, psopt, isopt, border, fit, ao, pspdf, ispdf,
- im2)
- setattr(TestImg2Pdf, "test_layout_%03d_im2" % i, layout_handler_im2)
-
- files = os.listdir(os.path.join(HERE, "input"))
- for with_pdfrw, test_name in [(a, b) for a in [True, False]
- for b in files]:
- # we do not test animation.gif with pdfrw because it doesn't support
- # saving hexadecimal palette data
- if test_name == 'animation.gif' and with_pdfrw:
- continue
- inputf = os.path.join(HERE, "input", test_name)
- if not os.path.isfile(inputf):
- continue
- outputf = os.path.join(HERE, "output", test_name+".pdf")
- assert os.path.isfile(outputf)
-
- def handle(self, f=inputf, out=outputf, with_pdfrw=with_pdfrw):
- with open(f, "rb") as inf:
- orig_imgdata = inf.read()
- output = img2pdf.convert(orig_imgdata, nodate=True,
- with_pdfrw=with_pdfrw)
- from pdfrw import PdfReader, PdfName, PdfWriter
- from pdfrw.py23_diffs import convert_load, convert_store
- x = PdfReader(PdfReaderIO(convert_load(output)))
- self.assertEqual(sorted(x.keys()), [PdfName.Info, PdfName.Root,
- PdfName.Size])
- self.assertIn(x.Root.Pages.Count, ('1', '2'))
- if len(x.Root.Pages.Kids) == '1':
- self.assertEqual(x.Size, '7')
- self.assertEqual(len(x.Root.Pages.Kids), 1)
- elif len(x.Root.Pages.Kids) == '2':
- self.assertEqual(x.Size, '10')
- self.assertEqual(len(x.Root.Pages.Kids), 2)
- self.assertEqual(x.Info, {})
- self.assertEqual(sorted(x.Root.keys()), [PdfName.Pages,
- PdfName.Type])
- self.assertEqual(x.Root.Type, PdfName.Catalog)
- self.assertEqual(sorted(x.Root.Pages.keys()),
- [PdfName.Count, PdfName.Kids, PdfName.Type])
- self.assertEqual(x.Root.Pages.Type, PdfName.Pages)
- orig_img = Image.open(f)
- for pagenum in range(len(x.Root.Pages.Kids)):
- # retrieve the original image frame that this page was
- # generated from
- orig_img.seek(pagenum)
- cur_page = x.Root.Pages.Kids[pagenum]
-
- ndpi = orig_img.info.get("dpi", (96.0, 96.0))
- # In python3, the returned dpi value for some tiff images will
- # not be an integer but a float. To make the behaviour of
- # img2pdf the same between python2 and python3, we convert that
- # float into an integer by rounding.
- # Search online for the 72.009 dpi problem for more info.
- ndpi = (int(round(ndpi[0])), int(round(ndpi[1])))
- imgwidthpx, imgheightpx = orig_img.size
- pagewidth = 72.0*imgwidthpx/ndpi[0]
- pageheight = 72.0*imgheightpx/ndpi[1]
-
- def format_float(f):
- if int(f) == f:
- return str(int(f))
- else:
- return ("%.4f" % f).rstrip("0")
-
- self.assertEqual(sorted(cur_page.keys()),
- [PdfName.Contents, PdfName.MediaBox,
- PdfName.Parent, PdfName.Resources,
- PdfName.Type])
- self.assertEqual(cur_page.MediaBox,
- ['0', '0', format_float(pagewidth),
- format_float(pageheight)])
- self.assertEqual(cur_page.Parent, x.Root.Pages)
- self.assertEqual(cur_page.Type, PdfName.Page)
- self.assertEqual(cur_page.Resources.keys(),
- [PdfName.XObject])
- self.assertEqual(cur_page.Resources.XObject.keys(),
- [PdfName.Im0])
- self.assertEqual(cur_page.Contents.keys(),
- [PdfName.Length])
- self.assertEqual(cur_page.Contents.Length,
- str(len(cur_page.Contents.stream)))
- self.assertEqual(cur_page.Contents.stream,
- "q\n%.4f 0 0 %.4f 0.0000 0.0000 cm\n"
- "/Im0 Do\nQ" % (pagewidth, pageheight))
-
- imgprops = cur_page.Resources.XObject.Im0
-
- # test if the filter is valid:
- self.assertIn(
- imgprops.Filter, [PdfName.DCTDecode, PdfName.JPXDecode,
- PdfName.FlateDecode,
- [PdfName.CCITTFaxDecode]])
-
- # test if the image has correct size
- self.assertEqual(imgprops.Width, str(orig_img.size[0]))
- self.assertEqual(imgprops.Height, str(orig_img.size[1]))
- # if the input file is a jpeg then it should've been copied
- # verbatim into the PDF
- if imgprops.Filter in [PdfName.DCTDecode,
- PdfName.JPXDecode]:
- self.assertEqual(
- cur_page.Resources.XObject.Im0.stream,
- convert_load(orig_imgdata))
- elif imgprops.Filter == [PdfName.CCITTFaxDecode]:
- tiff_header = tiff_header_for_ccitt(
- int(imgprops.Width), int(imgprops.Height),
- int(imgprops.Length), 4)
- imgio = BytesIO()
- imgio.write(tiff_header)
- imgio.write(convert_store(
- cur_page.Resources.XObject.Im0.stream))
- imgio.seek(0)
- im = Image.open(imgio)
- self.assertEqual(im.tobytes(), orig_img.tobytes())
- try:
- im.close()
- except AttributeError:
- pass
-
- elif imgprops.Filter == PdfName.FlateDecode:
- # otherwise, the data is flate encoded and has to be equal
- # to the pixel data of the input image
- imgdata = zlib.decompress(
- convert_store(cur_page.Resources.XObject.Im0.stream))
- if imgprops.DecodeParms:
- if orig_img.format == 'PNG':
- pngidat, palette = img2pdf.parse_png(orig_imgdata)
- elif orig_img.format == 'TIFF' \
- and orig_img.info['compression'] == "group4":
- offset, length = \
- img2pdf.ccitt_payload_location_from_pil(
- orig_img)
- pngidat = orig_imgdata[offset:offset+length]
- else:
- pngbuffer = BytesIO()
- orig_img.save(pngbuffer, format="png")
- pngidat, palette = img2pdf.parse_png(
- pngbuffer.getvalue())
- self.assertEqual(zlib.decompress(pngidat), imgdata)
- else:
- colorspace = imgprops.ColorSpace
- if colorspace == PdfName.DeviceGray:
- colorspace = 'L'
- elif colorspace == PdfName.DeviceRGB:
- colorspace = 'RGB'
- elif colorspace == PdfName.DeviceCMYK:
- colorspace = 'CMYK'
- else:
- raise Exception("invalid colorspace")
- im = Image.frombytes(colorspace,
- (int(imgprops.Width),
- int(imgprops.Height)),
- imgdata)
- if orig_img.mode == '1':
- self.assertEqual(im.tobytes(),
- orig_img.convert("L").tobytes())
- elif orig_img.mode not in ("RGB", "L", "CMYK",
- "CMYK;I"):
- self.assertEqual(im.tobytes(),
- orig_img.convert("RGB").tobytes())
- # the python-pil version 2.3.0-1ubuntu3 in Ubuntu does
- # not have the close() method
- try:
- im.close()
- except AttributeError:
- pass
- # now use pdfrw to parse and then write out both pdfs and check the
- # result for equality
- y = PdfReader(out)
- outx = BytesIO()
- outy = BytesIO()
- xwriter = PdfWriter()
- ywriter = PdfWriter()
- xwriter.trailer = x
- ywriter.trailer = y
- xwriter.write(outx)
- ywriter.write(outy)
- self.assertEqual(compare_pdf(outx.getvalue(), outy.getvalue()), True)
- # the python-pil version 2.3.0-1ubuntu3 in Ubuntu does not have the
- # close() method
- try:
- orig_img.close()
- except AttributeError:
- pass
- if with_pdfrw:
- setattr(TestImg2Pdf, "test_%s_with_pdfrw" % test_name, handle)
- else:
- setattr(TestImg2Pdf, "test_%s_without_pdfrw" % test_name, handle)
-
- return unittest.TestSuite((
- unittest.makeSuite(TestImg2Pdf),
- unittest.makeSuite(CommandLineTests),
- ))
diff --git a/src/tests/input/animation.gif b/src/tests/input/animation.gif
index af4b278..d60a237 100644
--- a/src/tests/input/animation.gif
+++ b/src/tests/input/animation.gif
Binary files differ
diff --git a/src/tests/output/animation.gif.pdf b/src/tests/output/animation.gif.pdf
index fdfd460..2af1ba4 100644
--- a/src/tests/output/animation.gif.pdf
+++ b/src/tests/output/animation.gif.pdf
Binary files differ
diff --git a/test.sh b/test.sh
deleted file mode 100755
index 5b34a30..0000000
--- a/test.sh
+++ /dev/null
@@ -1,1468 +0,0 @@
-#!/bin/sh
-
-set -eu
-
-similar()
-{
- psnr=$(compare -metric PSNR "$1" "$2" null: 2>&1 || true)
- if [ -z "$psnr" ]; then
- echo "compare failed"
- return 1
- fi
-
- # PSNR of zero means that they are identical
- if [ "$psnr" = 0 ]; then
- echo "images are equal -- don't use similar() but require exactness"
- exit 2
- fi
-
- # The lower PSNR value, the fewer the similarities
- # The lowest (and worst) value is 1.0
- min_psnr=50
- if [ "$min_psnr" != "$( printf "$psnr\n$min_psnr\n" | sort --general-numeric-sort | head --lines=1)" ]; then
- echo "pdf wrongly rendered"
- return 1
- fi
- return 0
-}
-
-compare_rendered()
-{
- pdf="$1"
- img="$2"
- gsdevice=png16m
- if [ "$#" -eq 3 ]; then
- gsdevice="$3"
- fi
-
- compare_ghostscript "$pdf" "$img" "$gsdevice"
-
- compare_poppler "$pdf" "$img"
-
- compare_mupdf "$pdf" "$img"
-}
-
-compare_ghostscript()
-{
- pdf="$1"
- img="$2"
- gsdevice="$3"
- gs -dQUIET -dNOPAUSE -dBATCH -sDEVICE="$gsdevice" -r96 -sOutputFile="$tempdir/gs-%00d.png" "$pdf"
- compare -metric AE "$img" "$tempdir/gs-1.png" null: 2>/dev/null
- rm "$tempdir/gs-1.png"
-}
-
-compare_poppler()
-{
- pdf="$1"
- img="$2"
- pdftocairo -r 96 -png "$pdf" "$tempdir/poppler"
- compare -metric AE "$img" "$tempdir/poppler-1.png" null: 2>/dev/null
- rm "$tempdir/poppler-1.png"
-}
-
-compare_mupdf()
-{
- pdf="$1"
- img="$2"
- mutool draw -o "$tempdir/mupdf.png" -r 96 "$pdf" 2>/dev/null
- compare -metric AE "$img" "$tempdir/mupdf.png" null: 2>/dev/null
- rm "$tempdir/mupdf.png"
-}
-
-compare_pdfimages()
-{
- pdf="$1"
- img="$2"
- pdfimages -png "$pdf" "$tempdir/images"
- compare -metric AE "$img" "$tempdir/images-000.png" null: 2>/dev/null
- rm "$tempdir/images-000.png"
-}
-
-error()
-{
- echo test $j failed
- echo intermediate data is left in $tempdir
- exit 1
-}
-
-tempdir=$(mktemp --directory --tmpdir img2pdf.XXXXXXXXXX)
-
-trap error EXIT
-
-# we use -strip to remove all timestamps (tIME chunk and exif data)
-convert -size 60x60 \( xc:none -fill red -draw 'circle 30,21 30,3' -gaussian-blur 0x3 \) \
- \( \( xc:none -fill lime -draw 'circle 39,39 36,57' -gaussian-blur 0x3 \) \
- \( xc:none -fill blue -draw 'circle 21,39 24,57' -gaussian-blur 0x3 \) \
- -compose plus -composite \
- \) -compose plus -composite \
- -strip \
- "$tempdir/alpha.png"
-
-convert "$tempdir/alpha.png" -background black -alpha remove -alpha off -strip "$tempdir/normal16.png"
-
-convert "$tempdir/normal16.png" -depth 8 -strip "$tempdir/normal.png"
-
-convert "$tempdir/normal.png" -negate -strip "$tempdir/inverse.png"
-
-convert "$tempdir/normal16.png" -colorspace Gray -depth 16 -strip "$tempdir/gray16.png"
-convert "$tempdir/normal16.png" -colorspace Gray -dither FloydSteinberg -colors 256 -depth 8 -strip "$tempdir/gray8.png"
-convert "$tempdir/normal16.png" -colorspace Gray -dither FloydSteinberg -colors 16 -depth 4 -strip "$tempdir/gray4.png"
-convert "$tempdir/normal16.png" -colorspace Gray -dither FloydSteinberg -colors 4 -depth 2 -strip "$tempdir/gray2.png"
-convert "$tempdir/normal16.png" -colorspace Gray -dither FloydSteinberg -colors 2 -depth 1 -strip "$tempdir/gray1.png"
-
-# use "-define png:exclude-chunk=bkgd" because otherwise, imagemagick will
-# add the background color (white) as an additional entry to the palette
-convert "$tempdir/normal.png" -dither FloydSteinberg -colors 2 -define png:exclude-chunk=bkgd -strip "$tempdir/palette1.png"
-convert "$tempdir/normal.png" -dither FloydSteinberg -colors 4 -define png:exclude-chunk=bkgd -strip "$tempdir/palette2.png"
-convert "$tempdir/normal.png" -dither FloydSteinberg -colors 16 -define png:exclude-chunk=bkgd -strip "$tempdir/palette4.png"
-convert "$tempdir/normal.png" -dither FloydSteinberg -colors 256 -define png:exclude-chunk=bkgd -strip "$tempdir/palette8.png"
-
-cat << END | ( cd "$tempdir"; md5sum --check --status - )
-a99ef2a356c315090b6939fa4ce70516 alpha.png
-0df21ebbce5292654119b17f6e52bc81 gray16.png
-6faee81b8db446caa5004ad71bddcb5b gray1.png
-97e423da517ede069348484a1283aa6c gray2.png
-cbed1b6da5183aec0b86909e82b77c41 gray4.png
-c0df42fdd69ae2a16ad0c23adb39895e gray8.png
-ac6bb850fb5aaee9fa7dcb67525cd0fc inverse.png
-3f3f8579f5054270e79a39e7cc4e89e0 normal16.png
-cbe63b21443af8321b213bde6666951f normal.png
-2f00705cca05fd94406fc39ede4d7322 palette1.png
-6cb250d1915c2af99c324c43ff8286eb palette2.png
-ab7b3d3907a851692ee36f5349ed0b2c palette4.png
-03829af4af8776adf56ba2e68f5b111e palette8.png
-END
-
-# use img2pdfprog environment variable if it is set
-if [ -z ${img2pdfprog+x} ]; then
- img2pdfprog=src/img2pdf.py
-fi
-
-img2pdf()
-{
- # we use --without-pdfrw to better "grep" the result and because we
- # cannot write palette based images otherwise
- $img2pdfprog --without-pdfrw --producer="" --nodate "$1" > "$2" 2>/dev/null
-}
-
-tests=51 # number of tests
-j=1 # current test
-
-###############################################################################
-echo "Test $j/$tests JPEG"
-
-convert "$tempdir/normal.png" "$tempdir/normal.jpg"
-
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Format: JPEG (Joint Photographic Experts Group JFIF format)$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Mime type: image/jpeg$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Compression: JPEG$'
-
-img2pdf "$tempdir/normal.jpg" "$tempdir/out.pdf"
-
-# We have to use jpegtopnm with the original JPG before being able to compare
-# it with imagemagick because imagemagick will decode the JPG slightly
-# differently than ghostscript, poppler and mupdf do it.
-# We have to use jpegtopnm and cannot use djpeg because the latter produces
-# slightly different results as well when called like this:
-# djpeg -dct int -pnm "$tempdir/normal.jpg" > "$tempdir/normal.pnm"
-# An alternative way to compare the JPG would be to require a different DCT
-# method when decoding by setting -define jpeg:dct-method=ifast in the
-# compare command.
-jpegtopnm -dct int "$tempdir/normal.jpg" > "$tempdir/normal.pnm" 2>/dev/null
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/normal.pnm"
-
-pdfimages -j "$tempdir/out.pdf" "$tempdir/images"
-cmp "$tempdir/normal.jpg" "$tempdir/images-000.jpg"
-rm "$tempdir/images-000.jpg"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceRGB$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /DCTDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/normal.jpg" "$tempdir/normal.pnm" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests JPEG (90° rotated)"
-
-convert "$tempdir/normal.png" "$tempdir/normal.jpg"
-exiftool -overwrite_original -all= "$tempdir/normal.jpg" -n >/dev/null
-exiftool -overwrite_original -Orientation=6 -XResolution=96 -YResolution=96 -n "$tempdir/normal.jpg" >/dev/null
-
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Format: JPEG (Joint Photographic Experts Group JFIF format)$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Mime type: image/jpeg$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Compression: JPEG$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ exif:Orientation: 6$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ exif:ResolutionUnit: 2$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ exif:XResolution: 96/1$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ exif:YResolution: 96/1$'
-
-img2pdf "$tempdir/normal.jpg" "$tempdir/out.pdf"
-
-# We have to use jpegtopnm with the original JPG before being able to compare
-# it with imagemagick because imagemagick will decode the JPG slightly
-# differently than ghostscript, poppler and mupdf do it.
-# We have to use jpegtopnm and cannot use djpeg because the latter produces
-# slightly different results as well when called like this:
-# djpeg -dct int -pnm "$tempdir/normal.jpg" > "$tempdir/normal.pnm"
-# An alternative way to compare the JPG would be to require a different DCT
-# method when decoding by setting -define jpeg:dct-method=ifast in the
-# compare command.
-jpegtopnm -dct int "$tempdir/normal.jpg" > "$tempdir/normal.pnm" 2>/dev/null
-convert -rotate "90" "$tempdir/normal.pnm" "$tempdir/normal_rotated.png"
-#convert -rotate "0" "$tempdir/normal.pnm" "$tempdir/normal_rotated.png"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/normal_rotated.png"
-
-pdfimages -j "$tempdir/out.pdf" "$tempdir/images"
-cmp "$tempdir/normal.jpg" "$tempdir/images-000.jpg"
-rm "$tempdir/images-000.jpg"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceRGB$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /DCTDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Rotate 90$' "$tempdir/out.pdf"
-
-rm "$tempdir/normal.jpg" "$tempdir/normal.pnm" "$tempdir/out.pdf" "$tempdir/normal_rotated.png"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests JPEG CMYK"
-
-convert "$tempdir/normal.png" -colorspace cmyk "$tempdir/normal.jpg"
-
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Format: JPEG (Joint Photographic Experts Group JFIF format)$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Mime type: image/jpeg$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Colorspace: CMYK$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Type: ColorSeparation$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.jpg" | grep --quiet '^ Compression: JPEG$'
-
-img2pdf "$tempdir/normal.jpg" "$tempdir/out.pdf"
-
-gs -dQUIET -dNOPAUSE -dBATCH -sDEVICE=tiff32nc -r96 -sOutputFile="$tempdir/gs-%00d.tiff" "$tempdir/out.pdf"
-similar "$tempdir/normal.jpg" "$tempdir/gs-1.tiff"
-rm "$tempdir/gs-1.tiff"
-
-# not testing with poppler as it cannot write CMYK images
-
-mutool draw -o "$tempdir/mupdf.pam" -r 96 -c cmyk "$pdf" 2>/dev/null
-similar "$tempdir/normal.jpg" "$tempdir/mupdf.pam"
-rm "$tempdir/mupdf.pam"
-
-pdfimages -j "$tempdir/out.pdf" "$tempdir/images"
-cmp "$tempdir/normal.jpg" "$tempdir/images-000.jpg"
-rm "$tempdir/images-000.jpg"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceCMYK$' "$tempdir/out.pdf"
-grep --quiet '^ /Decode \[ 1 0 1 0 1 0 1 0 \]$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /DCTDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/normal.jpg" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests JPEG2000"
-
-convert "$tempdir/normal.png" "$tempdir/normal.jp2"
-
-identify -verbose "$tempdir/normal.jp2" | grep --quiet '^ Format: JP2 (JPEG-2000 File Format Syntax)$'
-identify -verbose "$tempdir/normal.jp2" | grep --quiet '^ Mime type: image/jp2$'
-identify -verbose "$tempdir/normal.jp2" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.jp2" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/normal.jp2" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/normal.jp2" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/normal.jp2" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.jp2" | grep --quiet '^ Compression: JPEG2000$'
-
-img2pdf "$tempdir/normal.jp2" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/normal.jp2"
-
-pdfimages -jp2 "$tempdir/out.pdf" "$tempdir/images"
-cmp "$tempdir/normal.jp2" "$tempdir/images-000.jp2"
-rm "$tempdir/images-000.jp2"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceRGB$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /JPXDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/normal.jp2" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-#echo Test JPEG2000 CMYK
-#
-# cannot test because imagemagick does not support JPEG2000 CMYK
-
-###############################################################################
-echo "Test $j/$tests PNG RGB8"
-
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ Mime type: image/png$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ png:IHDR.bit-depth-orig: 8$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ png:IHDR.bit_depth: 8$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ png:IHDR.color-type-orig: 2$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ png:IHDR.color_type: 2 (Truecolor)$'
-identify -verbose "$tempdir/normal.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
-img2pdf "$tempdir/normal.png" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/normal.png"
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/normal.png"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceRGB$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /Colors 3$' "$tempdir/out.pdf"
-grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests PNG RGB16"
-
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ Mime type: image/png$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ Depth: 16-bit$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ png:IHDR.bit-depth-orig: 16$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ png:IHDR.bit_depth: 16$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ png:IHDR.color-type-orig: 2$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ png:IHDR.color_type: 2 (Truecolor)$'
-identify -verbose "$tempdir/normal16.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
-img2pdf "$tempdir/normal16.png" "$tempdir/out.pdf"
-
-compare_ghostscript "$tempdir/out.pdf" "$tempdir/normal16.png" tiff48nc
-
-# poppler outputs 8-bit RGB so the comparison will not be exact
-pdftocairo -r 96 -png "$tempdir/out.pdf" "$tempdir/poppler"
-similar "$tempdir/normal16.png" "$tempdir/poppler-1.png"
-rm "$tempdir/poppler-1.png"
-
-# pdfimages is unable to write 16 bit output
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 16$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceRGB$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 16$' "$tempdir/out.pdf"
-grep --quiet '^ /Colors 3$' "$tempdir/out.pdf"
-grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests PNG RGBA8"
-
-convert "$tempdir/alpha.png" -depth 8 -strip "$tempdir/alpha8.png"
-
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ Mime type: image/png$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ Type: TrueColorAlpha$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ png:IHDR.bit-depth-orig: 8$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ png:IHDR.bit_depth: 8$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ png:IHDR.color-type-orig: 6$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ png:IHDR.color_type: 6 (RGBA)$'
-identify -verbose "$tempdir/alpha8.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
-img2pdf "$tempdir/alpha8.png" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/alpha8.png"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests PNG RGBA16"
-
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ Mime type: image/png$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ Type: TrueColorAlpha$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ Depth: 16-bit$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ png:IHDR.bit-depth-orig: 16$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ png:IHDR.bit_depth: 16$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ png:IHDR.color-type-orig: 6$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ png:IHDR.color_type: 6 (RGBA)$'
-identify -verbose "$tempdir/alpha.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
-img2pdf "$tempdir/alpha.png" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests PNG Gray8 Alpha"
-
-convert "$tempdir/alpha.png" -colorspace Gray -dither FloydSteinberg -colors 256 -depth 8 -strip "$tempdir/alpha_gray8.png"
-
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ Mime type: image/png$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ Colorspace: Gray$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ Type: GrayscaleAlpha$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ png:IHDR.bit-depth-orig: 8$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ png:IHDR.bit_depth: 8$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ png:IHDR.color-type-orig: 4$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ png:IHDR.color_type: 4 (GrayAlpha)$'
-identify -verbose "$tempdir/alpha_gray8.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
-img2pdf "$tempdir/alpha_gray8.png" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/alpha_gray8.png"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests PNG Gray16 Alpha"
-
-convert "$tempdir/alpha.png" -colorspace Gray -depth 16 -strip "$tempdir/alpha_gray16.png"
-
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ Mime type: image/png$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ Colorspace: Gray$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ Type: GrayscaleAlpha$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ Depth: 16-bit$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ png:IHDR.bit-depth-orig: 16$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ png:IHDR.bit_depth: 16$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ png:IHDR.color-type-orig: 4$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ png:IHDR.color_type: 4 (GrayAlpha)$'
-identify -verbose "$tempdir/alpha_gray16.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
-img2pdf "$tempdir/alpha_gray16.png" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/alpha_gray16.png"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests PNG interlaced"
-
-convert "$tempdir/normal.png" -interlace PNG -strip "$tempdir/interlace.png"
-
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ Mime type: image/png$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ png:IHDR.bit-depth-orig: 8$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ png:IHDR.bit_depth: 8$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ png:IHDR.color-type-orig: 2$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ png:IHDR.color_type: 2 (Truecolor)$'
-identify -verbose "$tempdir/interlace.png" | grep --quiet '^ png:IHDR.interlace_method: 1 (Adam7 method)$'
-
-img2pdf "$tempdir/interlace.png" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/normal.png"
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/normal.png"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceRGB$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /Colors 3$' "$tempdir/out.pdf"
-grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/interlace.png" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-for i in 1 2 4 8; do
- echo "Test $j/$tests PNG Gray$i"
-
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ Mime type: image/png$'
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ Geometry: 60x60+0+0$'
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ Colorspace: Gray$'
- if [ "$i" -eq 1 ]; then
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ Type: Bilevel$'
- else
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ Type: Grayscale$'
- fi
- if [ "$i" -eq 8 ]; then
- identify -verbose "$tempdir/gray$i.png" | grep --quiet "^ Depth: 8-bit$"
- else
- identify -verbose "$tempdir/gray$i.png" | grep --quiet "^ Depth: 8/$i-bit$"
- fi
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ Compression: Zip$'
- identify -verbose "$tempdir/gray$i.png" | grep --quiet "^ png:IHDR.bit-depth-orig: $i$"
- identify -verbose "$tempdir/gray$i.png" | grep --quiet "^ png:IHDR.bit_depth: $i$"
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ png:IHDR.color-type-orig: 0$'
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ png:IHDR.color_type: 0 (Grayscale)$'
- identify -verbose "$tempdir/gray$i.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
- img2pdf "$tempdir/gray$i.png" "$tempdir/out.pdf"
-
- compare_rendered "$tempdir/out.pdf" "$tempdir/gray$i.png" pnggray
-
- compare_pdfimages "$tempdir/out.pdf" "$tempdir/gray$i.png"
-
- grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent '"$i"'$' "$tempdir/out.pdf"
- grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent '"$i"'$' "$tempdir/out.pdf"
- grep --quiet '^ /Colors 1$' "$tempdir/out.pdf"
- grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
- grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
- grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
- grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
- rm "$tempdir/out.pdf"
- j=$((j+1))
-done
-
-###############################################################################
-echo "Test $j/$tests PNG Gray16"
-
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ Mime type: image/png$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ Colorspace: Gray$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ Type: Grayscale$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ Depth: 16-bit$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ png:IHDR.bit-depth-orig: 16$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ png:IHDR.bit_depth: 16$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ png:IHDR.color-type-orig: 0$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ png:IHDR.color_type: 0 (Grayscale)$'
-identify -verbose "$tempdir/gray16.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
-img2pdf "$tempdir/gray16.png" "$tempdir/out.pdf"
-
-# ghostscript outputs 8-bit grayscale, so the comparison will not be exact
-gs -dQUIET -dNOPAUSE -dBATCH -sDEVICE=pnggray -r96 -sOutputFile="$tempdir/gs-%00d.png" "$tempdir/out.pdf"
-similar "$tempdir/gray16.png" "$tempdir/gs-1.png"
-rm "$tempdir/gs-1.png"
-
-# poppler outputs 8-bit grayscale so the comparison will not be exact
-pdftocairo -r 96 -png "$tempdir/out.pdf" "$tempdir/poppler"
-similar "$tempdir/gray16.png" "$tempdir/poppler-1.png"
-rm "$tempdir/poppler-1.png"
-
-# pdfimages outputs 8-bit grayscale so the comparison will not be exact
-pdfimages -png "$tempdir/out.pdf" "$tempdir/images"
-similar "$tempdir/gray16.png" "$tempdir/images-000.png"
-rm "$tempdir/images-000.png"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 16$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 16$' "$tempdir/out.pdf"
-grep --quiet '^ /Colors 1$' "$tempdir/out.pdf"
-grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-for i in 1 2 4 8; do
- echo "Test $j/$tests PNG Palette$i"
-
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ Format: PNG (Portable Network Graphics)$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ Mime type: image/png$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ Geometry: 60x60+0+0$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ Colorspace: sRGB$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ Type: Palette$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ Depth: 8-bit$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ Page geometry: 60x60+0+0$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ Compression: Zip$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet "^ png:IHDR.bit-depth-orig: $i$"
- identify -verbose "$tempdir/palette$i.png" | grep --quiet "^ png:IHDR.bit_depth: $i$"
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ png:IHDR.color-type-orig: 3$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ png:IHDR.color_type: 3 (Indexed)$'
- identify -verbose "$tempdir/palette$i.png" | grep --quiet '^ png:IHDR.interlace_method: 0 (Not interlaced)$'
-
- img2pdf "$tempdir/palette$i.png" "$tempdir/out.pdf"
-
- compare_rendered "$tempdir/out.pdf" "$tempdir/palette$i.png"
-
- # pdfimages cannot export palette based images
-
- grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent '"$i"'$' "$tempdir/out.pdf"
- grep --quiet '^ /ColorSpace \[ /Indexed /DeviceRGB ' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent '"$i"'$' "$tempdir/out.pdf"
- grep --quiet '^ /Colors 1$' "$tempdir/out.pdf"
- grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
- grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
- grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
- grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
- rm "$tempdir/out.pdf"
- j=$((j+1))
-done
-
-###############################################################################
-echo "Test $j/$tests GIF transparent"
-
-convert "$tempdir/alpha.png" "$tempdir/alpha.gif"
-
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Format: GIF (CompuServe graphics interchange format)$'
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Mime type: image/gif$'
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Type: PaletteAlpha$'
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Colormap entries: 256$'
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha.gif" | grep --quiet '^ Compression: LZW$'
-
-img2pdf "$tempdir/alpha.gif" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/alpha.gif"
-j=$((j+1))
-
-###############################################################################
-for i in 1 2 4 8; do
- echo "Test $j/$tests GIF Palette$i"
-
- convert "$tempdir/palette$i.png" "$tempdir/palette$i.gif"
-
- identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Format: GIF (CompuServe graphics interchange format)$'
- identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Mime type: image/gif$'
- identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Geometry: 60x60+0+0$'
- identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Colorspace: sRGB$'
- identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Type: Palette$'
- identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Depth: 8-bit$'
- case $i in
- 1) identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Colormap entries: 2$';;
- 2) identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Colormap entries: 4$';;
- 4) identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Colormap entries: 16$';;
- 8) identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Colormap entries: 256$';;
- esac
- identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Page geometry: 60x60+0+0$'
- identify -verbose "$tempdir/palette$i.gif" | grep --quiet '^ Compression: LZW$'
-
- img2pdf "$tempdir/palette$i.gif" "$tempdir/out.pdf"
-
- compare_rendered "$tempdir/out.pdf" "$tempdir/palette$i.png"
-
- # pdfimages cannot export palette based images
-
- grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent '"$i"'$' "$tempdir/out.pdf"
- grep --quiet '^ /ColorSpace \[ /Indexed /DeviceRGB ' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent '"$i"'$' "$tempdir/out.pdf"
- grep --quiet '^ /Colors 1$' "$tempdir/out.pdf"
- grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
- grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
- grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
- grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
- rm "$tempdir/out.pdf" "$tempdir/palette$i.gif"
- j=$((j+1))
-done
-
-###############################################################################
-echo "Test $j/$tests GIF animation"
-
-convert "$tempdir/normal.png" "$tempdir/inverse.png" -strip "$tempdir/animation.gif"
-
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Format: GIF (CompuServe graphics interchange format)$'
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Mime type: image/gif$'
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Type: Palette$'
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Colormap entries: 256$'
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/animation.gif[0]" | grep --quiet '^ Compression: LZW$'
-
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Format: GIF (CompuServe graphics interchange format)$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Mime type: image/gif$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Type: Palette$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Colormap entries: 256$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Compression: LZW$'
-identify -verbose "$tempdir/animation.gif[1]" | grep --quiet '^ Scene: 1$'
-
-img2pdf "$tempdir/animation.gif" "$tempdir/out.pdf"
-
-if [ "$(pdfinfo "$tempdir/out.pdf" | awk '/Pages:/ {print $2}')" != 2 ]; then
- echo "pdf does not have 2 pages"
- exit 1
-fi
-
-pdfseparate "$tempdir/out.pdf" "$tempdir/page-%d.pdf"
-rm "$tempdir/out.pdf"
-
-for page in 1 2; do
- compare_rendered "$tempdir/page-$page.pdf" "$tempdir/animation.gif[$((page-1))]"
-
- # pdfimages cannot export palette based images
-
- # We cannot grep the PDF metadata here, because the page was
- # rewritten into a non-greppable format by pdfseparate. but that's
- # okay, because we already grepped single pages before and multipage
- # PDF should not be different.
-
- rm "$tempdir/page-$page.pdf"
-done
-
-rm "$tempdir/animation.gif"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF float"
-
-convert "$tempdir/normal.png" -depth 32 -define quantum:format=floating-point "$tempdir/float.tiff"
-
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Depth: 32/8-bit$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ quantum:format: floating-point$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/float.tiff" | grep --quiet '^ tiff:photometric: RGB$'
-
-img2pdf "$tempdir/float.tiff" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/float.tiff"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF CMYK8"
-
-convert "$tempdir/normal.png" -colorspace cmyk "$tempdir/cmyk8.tiff"
-
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Colorspace: CMYK$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Type: ColorSeparation$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/cmyk8.tiff" | grep --quiet '^ tiff:photometric: separated$'
-
-img2pdf "$tempdir/cmyk8.tiff" "$tempdir/out.pdf"
-
-compare_ghostscript "$tempdir/out.pdf" "$tempdir/cmyk8.tiff" tiff32nc
-
-# not testing with poppler as it cannot write CMYK images
-
-mutool draw -o "$tempdir/mupdf.pam" -r 96 -c cmyk "$pdf" 2>/dev/null
-compare -metric AE "$tempdir/cmyk8.tiff" "$tempdir/mupdf.pam" null: 2>/dev/null
-rm "$tempdir/mupdf.pam"
-
-pdfimages -tiff "$tempdir/out.pdf" "$tempdir/images"
-compare -metric AE "$tempdir/cmyk8.tiff" "$tempdir/images-000.tif" null: 2>/dev/null
-rm "$tempdir/images-000.tif"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceCMYK$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/cmyk8.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF CMYK16"
-
-convert "$tempdir/normal.png" -depth 16 -colorspace cmyk "$tempdir/cmyk16.tiff"
-
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Colorspace: CMYK$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Type: ColorSeparation$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Depth: 16-bit$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/cmyk16.tiff" | grep --quiet '^ tiff:photometric: separated$'
-
-# PIL is unable to read 16 bit CMYK images
-img2pdf "$tempdir/cmyk16.gif" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/cmyk16.tiff"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF RGB8"
-
-convert "$tempdir/normal.png" "$tempdir/normal.tiff"
-
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/normal.tiff" | grep --quiet '^ tiff:photometric: RGB$'
-
-img2pdf "$tempdir/normal.tiff" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/normal.tiff" tiff24nc
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/normal.tiff"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceRGB$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
-grep --quiet '^ /Colors 3$' "$tempdir/out.pdf"
-grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/normal.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF RGBA8"
-
-convert "$tempdir/alpha.png" -depth 8 -strip "$tempdir/alpha8.tiff"
-
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Type: TrueColorAlpha$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ tiff:alpha: unassociated$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/alpha8.tiff" | grep --quiet '^ tiff:photometric: RGB$'
-
-img2pdf "$tempdir/alpha8.tiff" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/alpha8.tiff"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF RGBA16"
-
-convert "$tempdir/alpha.png" -strip "$tempdir/alpha16.tiff"
-
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Type: TrueColorAlpha$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Depth: 16-bit$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ tiff:alpha: unassociated$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/alpha16.tiff" | grep --quiet '^ tiff:photometric: RGB$'
-
-img2pdf "$tempdir/alpha16.tiff" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/alpha16.tiff"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF Gray1"
-
-convert "$tempdir/gray1.png" -depth 1 "$tempdir/gray1.tiff"
-
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Colorspace: Gray$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Type: Bilevel$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Depth: 1-bit$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/gray1.tiff" | grep --quiet '^ tiff:photometric: min-is-black$'
-
-img2pdf "$tempdir/gray1.tiff" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/gray1.png" pnggray
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/gray1.png"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 1$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
-grep --quiet '^ /BlackIs1 true$' "$tempdir/out.pdf"
-grep --quiet '^ /Columns 60$' "$tempdir/out.pdf"
-grep --quiet '^ /K -1$' "$tempdir/out.pdf"
-grep --quiet '^ /Rows 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter \[ /CCITTFaxDecode \]$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/gray1.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-for i in 2 4 8; do
- echo "Test $j/$tests TIFF Gray$i"
-
- convert "$tempdir/gray$i.png" -depth $i "$tempdir/gray$i.tiff"
-
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ Mime type: image/tiff$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ Colorspace: Gray$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ Type: Grayscale$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ Endianess: LSB$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet "^ Depth: $i-bit$"
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ Compression: Zip$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ tiff:endian: lsb$'
- identify -verbose "$tempdir/gray$i.tiff" | grep --quiet '^ tiff:photometric: min-is-black$'
-
- img2pdf "$tempdir/gray$i.tiff" "$tempdir/out.pdf"
-
- compare_rendered "$tempdir/out.pdf" "$tempdir/gray$i.png" pnggray
-
- compare_pdfimages "$tempdir/out.pdf" "$tempdir/gray$i.png"
-
- # When saving a PNG, PIL will store it as 8-bit data
- grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
- grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent 8$' "$tempdir/out.pdf"
- grep --quiet '^ /Colors 1$' "$tempdir/out.pdf"
- grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
- grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
- grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
- grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
- rm "$tempdir/gray$i.tiff" "$tempdir/out.pdf"
- j=$((j+1))
-done
-
-################################################################################
-echo "Test $j/$tests TIFF Gray16"
-
-convert "$tempdir/gray16.png" -depth 16 "$tempdir/gray16.tiff"
-
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ Colorspace: Gray$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ Type: Grayscale$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet "^ Depth: 16-bit$"
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/gray16.tiff" | grep --quiet '^ tiff:photometric: min-is-black$'
-
-img2pdf "$tempdir/gray16.tiff" /dev/null && rc=$? || rc=$?
-if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
-fi
-
-rm "$tempdir/gray16.tiff"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF multipage"
-
-convert "$tempdir/normal.png" "$tempdir/inverse.png" -strip "$tempdir/multipage.tiff"
-
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ tiff:alpha: unspecified$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/multipage.tiff[0]" | grep --quiet '^ tiff:photometric: RGB$'
-
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Mime type: image/tiff$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Geometry: 60x60+0+0$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Colorspace: sRGB$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Type: TrueColor$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Endianess: LSB$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Depth: 8-bit$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Page geometry: 60x60+0+0$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Compression: Zip$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ tiff:alpha: unspecified$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ tiff:endian: lsb$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ tiff:photometric: RGB$'
-identify -verbose "$tempdir/multipage.tiff[1]" | grep --quiet '^ Scene: 1$'
-
-img2pdf "$tempdir/multipage.tiff" "$tempdir/out.pdf"
-
-if [ "$(pdfinfo "$tempdir/out.pdf" | awk '/Pages:/ {print $2}')" != 2 ]; then
- echo "pdf does not have 2 pages"
- exit 1
-fi
-
-pdfseparate "$tempdir/out.pdf" "$tempdir/page-%d.pdf"
-rm "$tempdir/out.pdf"
-
-for page in 1 2; do
- compare_rendered "$tempdir/page-$page.pdf" "$tempdir/multipage.tiff[$((page-1))]"
-
- compare_pdfimages "$tempdir/page-$page.pdf" "$tempdir/multipage.tiff[$((page-1))]"
-
- # We cannot grep the PDF metadata here, because the page was
- # rewritten into a non-greppable format by pdfseparate. but that's
- # okay, because we already grepped single pages before and multipage
- # PDF should not be different.
-
- rm "$tempdir/page-$page.pdf"
-done
-
-rm "$tempdir/multipage.tiff"
-j=$((j+1))
-
-###############################################################################
-for i in 1 2 4 8; do
- echo "Test $j/$tests TIFF Palette$i"
-
- convert "$tempdir/palette$i.png" "$tempdir/palette$i.tiff"
-
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Mime type: image/tiff$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Colorspace: sRGB$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Type: Palette$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Endianess: LSB$'
- if [ "$i" -eq 8 ]; then
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet "^ Depth: 8-bit$"
- else
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet "^ Depth: $i/8-bit$"
- fi
- case $i in
- 1) identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Colormap entries: 2$';;
- 2) identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Colormap entries: 4$';;
- 4) identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Colormap entries: 16$';;
- 8) identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Colormap entries: 256$';;
- esac
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ Compression: Zip$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ tiff:endian: lsb$'
- identify -verbose "$tempdir/palette$i.tiff" | grep --quiet '^ tiff:photometric: palette$'
-
- img2pdf "$tempdir/palette$i.tiff" "$tempdir/out.pdf"
-
- compare_rendered "$tempdir/out.pdf" "$tempdir/palette$i.png"
-
- # pdfimages cannot export palette based images
-
- grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent '"$i"'$' "$tempdir/out.pdf"
- grep --quiet '^ /ColorSpace \[ /Indexed /DeviceRGB ' "$tempdir/out.pdf"
- grep --quiet '^ /BitsPerComponent '"$i"'$' "$tempdir/out.pdf"
- grep --quiet '^ /Colors 1$' "$tempdir/out.pdf"
- grep --quiet '^ /Predictor 15$' "$tempdir/out.pdf"
- grep --quiet '^ /Filter /FlateDecode$' "$tempdir/out.pdf"
- grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
- grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
- rm "$tempdir/out.pdf"
-
- rm "$tempdir/palette$i.tiff"
- j=$((j+1))
-done
-
-###############################################################################
-for i in 12 14 16; do
- echo "Test $j/$tests TIFF RGB$i"
-
- convert "$tempdir/normal16.png" -depth "$i" "$tempdir/normal$i.tiff"
-
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ Format: TIFF (Tagged Image File Format)$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ Mime type: image/tiff$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ Geometry: 60x60+0+0$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ Colorspace: sRGB$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ Type: TrueColor$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ Endianess: LSB$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet "^ Depth: $i-bit$"
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ Page geometry: 60x60+0+0$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ Compression: Zip$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ tiff:alpha: unspecified$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ tiff:endian: lsb$'
- identify -verbose "$tempdir/normal$i.tiff" | grep --quiet '^ tiff:photometric: RGB$'
-
- img2pdf "$tempdir/normal$i.tiff" /dev/null && rc=$? || rc=$?
- if [ "$rc" -eq 0 ]; then
- echo needs to fail here
- exit 1
- fi
-
- rm "$tempdir/normal$i.tiff"
- j=$((j+1))
-done
-
-###############################################################################
-echo "Test $j/$tests TIFF CCITT Group4, little endian, msb-to-lsb, min-is-white"
-
-convert "$tempdir/gray1.png" -compress group4 -define tiff:endian=lsb -define tiff:fill-order=msb -define quantum:polarity=min-is-white "$tempdir/group4.tiff"
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Bits/Sample: 1'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Compression Scheme: CCITT Group 4'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Photometric Interpretation: min-is-white'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'FillOrder: msb-to-lsb'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Samples/Pixel: 1'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Type: Bilevel'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Endianess: LSB'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Depth: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'gray: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Compression: Group4'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:endian: lsb'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:photometric: min-is-white'
-
-img2pdf "$tempdir/group4.tiff" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/group4.tiff" pnggray
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/group4.tiff"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 1$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
-grep --quiet '^ /BlackIs1 false$' "$tempdir/out.pdf"
-grep --quiet '^ /Columns 60$' "$tempdir/out.pdf"
-grep --quiet '^ /K -1$' "$tempdir/out.pdf"
-grep --quiet '^ /Rows 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter \[ /CCITTFaxDecode \]$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/group4.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF CCITT Group4, big endian, msb-to-lsb, min-is-white"
-
-convert "$tempdir/gray1.png" -compress group4 -define tiff:endian=msb -define tiff:fill-order=msb -define quantum:polarity=min-is-white "$tempdir/group4.tiff"
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Bits/Sample: 1'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Compression Scheme: CCITT Group 4'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Photometric Interpretation: min-is-white'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'FillOrder: msb-to-lsb'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Samples/Pixel: 1'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Type: Bilevel'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Endianess: MSB'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Depth: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'gray: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Compression: Group4'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:endian: msb'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:photometric: min-is-white'
-
-img2pdf "$tempdir/group4.tiff" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/group4.tiff" pnggray
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/group4.tiff"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 1$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
-grep --quiet '^ /BlackIs1 false$' "$tempdir/out.pdf"
-grep --quiet '^ /Columns 60$' "$tempdir/out.pdf"
-grep --quiet '^ /K -1$' "$tempdir/out.pdf"
-grep --quiet '^ /Rows 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter \[ /CCITTFaxDecode \]$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/group4.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF CCITT Group4, big endian, lsb-to-msb, min-is-white"
-
-convert "$tempdir/gray1.png" -compress group4 -define tiff:endian=msb -define tiff:fill-order=lsb -define quantum:polarity=min-is-white "$tempdir/group4.tiff"
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Bits/Sample: 1'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Compression Scheme: CCITT Group 4'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Photometric Interpretation: min-is-white'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'FillOrder: lsb-to-msb'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Samples/Pixel: 1'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Type: Bilevel'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Endianess: MSB'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Depth: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'gray: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Compression: Group4'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:endian: msb'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:photometric: min-is-white'
-
-img2pdf "$tempdir/group4.tiff" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/group4.tiff" pnggray
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/group4.tiff"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 1$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
-grep --quiet '^ /BlackIs1 false$' "$tempdir/out.pdf"
-grep --quiet '^ /Columns 60$' "$tempdir/out.pdf"
-grep --quiet '^ /K -1$' "$tempdir/out.pdf"
-grep --quiet '^ /Rows 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter \[ /CCITTFaxDecode \]$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/group4.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF CCITT Group4, little endian, msb-to-lsb, min-is-black"
-
-# We create a min-is-black group4 tiff with PIL because it creates these by
-# default (and without the option to do otherwise) whereas imagemagick only
-# became able to do it through commit 00730551f0a34328685c59d0dde87dd9e366103a
-# See https://www.imagemagick.org/discourse-server/viewtopic.php?f=1&t=34605
-python3 -c 'from PIL import Image;Image.open("'"$tempdir/gray1.png"'").save("'"$tempdir/group4.tiff"'",format="TIFF",compression="group4")'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Bits/Sample: 1'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Compression Scheme: CCITT Group 4'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Photometric Interpretation: min-is-black'
-# PIL doesn't set those
-#tiffinfo "$tempdir/group4.tiff" | grep --quiet 'FillOrder: msb-to-lsb'
-#tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Samples/Pixel: 1'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Type: Bilevel'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Endianess: LSB'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Depth: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'gray: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Compression: Group4'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:endian: lsb'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:photometric: min-is-black'
-
-img2pdf "$tempdir/group4.tiff" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/group4.tiff" pnggray
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/group4.tiff"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 1$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
-grep --quiet '^ /BlackIs1 true$' "$tempdir/out.pdf"
-grep --quiet '^ /Columns 60$' "$tempdir/out.pdf"
-grep --quiet '^ /K -1$' "$tempdir/out.pdf"
-grep --quiet '^ /Rows 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter \[ /CCITTFaxDecode \]$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/group4.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF CCITT Group4, without fillorder, samples/pixel, bits/sample"
-
-convert "$tempdir/gray1.png" -compress group4 -define tiff:endian=lsb -define tiff:fill-order=msb -define quantum:polarity=min-is-white "$tempdir/group4.tiff"
-# remove BitsPerSample (258)
-tiffset -u 258 "$tempdir/group4.tiff"
-# remove FillOrder (266)
-tiffset -u 266 "$tempdir/group4.tiff"
-# remove SamplesPerPixel (277)
-tiffset -u 277 "$tempdir/group4.tiff"
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Bits/Sample: 1' && exit 1
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Compression Scheme: CCITT Group 4'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Photometric Interpretation: min-is-white'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'FillOrder: msb-to-lsb' && exit 1
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Samples/Pixel: 1' && exit 1
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Type: Bilevel'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Endianess: LSB'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Depth: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'gray: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Compression: Group4'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:endian: lsb'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:photometric: min-is-white'
-
-img2pdf "$tempdir/group4.tiff" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/group4.tiff" pnggray
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/group4.tiff"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 1$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
-grep --quiet '^ /BlackIs1 false$' "$tempdir/out.pdf"
-grep --quiet '^ /Columns 60$' "$tempdir/out.pdf"
-grep --quiet '^ /K -1$' "$tempdir/out.pdf"
-grep --quiet '^ /Rows 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter \[ /CCITTFaxDecode \]$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/group4.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-###############################################################################
-echo "Test $j/$tests TIFF CCITT Group4, without rows-per-strip"
-
-convert "$tempdir/gray1.png" -compress group4 -define tiff:endian=lsb -define tiff:fill-order=msb -define quantum:polarity=min-is-white -define tiff:rows-per-strip=4294967295 "$tempdir/group4.tiff"
-# remove RowsPerStrip (278)
-tiffset -u 278 "$tempdir/group4.tiff"
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Bits/Sample: 1'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Compression Scheme: CCITT Group 4'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Photometric Interpretation: min-is-white'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'FillOrder: msb-to-lsb'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Samples/Pixel: 1'
-tiffinfo "$tempdir/group4.tiff" | grep --quiet 'Rows/Strip:' && exit 1
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Type: Bilevel'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Endianess: LSB'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Depth: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'gray: 1-bit'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'Compression: Group4'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:endian: lsb'
-identify -verbose "$tempdir/group4.tiff" | grep --quiet 'tiff:photometric: min-is-white'
-
-img2pdf "$tempdir/group4.tiff" "$tempdir/out.pdf"
-
-compare_rendered "$tempdir/out.pdf" "$tempdir/group4.tiff" pnggray
-
-compare_pdfimages "$tempdir/out.pdf" "$tempdir/group4.tiff"
-
-grep --quiet '^45.0000 0 0 45.0000 0.0000 0.0000 cm$' "$tempdir/out.pdf"
-grep --quiet '^ /BitsPerComponent 1$' "$tempdir/out.pdf"
-grep --quiet '^ /ColorSpace /DeviceGray$' "$tempdir/out.pdf"
-grep --quiet '^ /BlackIs1 false$' "$tempdir/out.pdf"
-grep --quiet '^ /Columns 60$' "$tempdir/out.pdf"
-grep --quiet '^ /K -1$' "$tempdir/out.pdf"
-grep --quiet '^ /Rows 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Filter \[ /CCITTFaxDecode \]$' "$tempdir/out.pdf"
-grep --quiet '^ /Height 60$' "$tempdir/out.pdf"
-grep --quiet '^ /Width 60$' "$tempdir/out.pdf"
-
-rm "$tempdir/group4.tiff" "$tempdir/out.pdf"
-j=$((j+1))
-
-rm "$tempdir/alpha.png" "$tempdir/normal.png" "$tempdir/inverse.png" "$tempdir/palette1.png" "$tempdir/palette2.png" "$tempdir/palette4.png" "$tempdir/palette8.png" "$tempdir/gray8.png" "$tempdir/normal16.png" "$tempdir/gray16.png" "$tempdir/gray4.png" "$tempdir/gray2.png" "$tempdir/gray1.png"
-rmdir "$tempdir"
-
-trap - EXIT