Skip to content

Commit

Permalink
Added support for embedding indexed images (#443)
Browse files Browse the repository at this point in the history
* added support for palette images

* added test images palette

* removed generate=true

* removed link=None

* fixed previous tests

* removed Trailing whitespace

* testing image filters

* managed "PA" images

* small fix

* generating pdf with pillow 9.1.1

* unsupported image filter error test

* allowing embedding of indexed PNG images

* formatting

* added PIL image

* added test for p/pa images

* porting transparency in the pdf of P images

* fixed typos

* calling get_img_info

* removed trailing whitespace

* fixed image info

* fixed pdfs

Co-authored-by: Lucas Cimon <925560+Lucas-C@users.noreply.github.com>
  • Loading branch information
RedShy and Lucas-C committed May 27, 2022
1 parent 3046856 commit d2fa052
Show file tree
Hide file tree
Showing 25 changed files with 1,720 additions and 1,625 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Expand Up @@ -25,6 +25,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
- allowing correctly parsing of SVG files with CSS styling (`style="..."` attribute), thanks to @RedShy
- [`FPDF.star`](https://pyfpdf.github.io/fpdf2/Shapes.html#regular-star): new method added to draw regular stars, thanks to @digidigital and @RedShy
- [`FPDF.ink_annotation`](https://pyfpdf.github.io/fpdf2/Annotations.html#ink-annotations): new method added to add path annotations
- allowing embedding of indexed PNG images without converting them to RGB colorspace, thanks to @RedShy
- allowing to change appearance of [highlight annotations](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.highlight) by specifying a [`TextMarkupType`](https://pyfpdf.github.io/fpdf2/fpdf/enums.html#fpdf.enums.TextMarkupType)
- documentation on how to control objects transparency: [link to docs](https://pyfpdf.github.io/fpdf2/Transparency.html)
- documentation on how to create tables and charts using [pandas](https://pandas.pydata.org/) DataFrames: [link to docs](https://pyfpdf.github.io/fpdf2/Maths.html), thanks to @iwayankurniawan
Expand Down
16 changes: 10 additions & 6 deletions fpdf/fpdf.py
Expand Up @@ -4000,9 +4000,14 @@ def _putimage(self, info):
self._out(f"/Height {info['h']}")

if info["cs"] == "Indexed":
palette_ref = (
pdf_ref(self.n + 2)
if self.allow_images_transparency and "smask" in info
else pdf_ref(self.n + 1)
)
self._out(
f"/ColorSpace [/Indexed /DeviceRGB "
f"{len(info['pal']) // 3 - 1} {pdf_ref(self.n + 1)}]"
f"{len(info['pal']) // 3 - 1} {palette_ref}]"
)
else:
self._out(f"/ColorSpace /{info['cs']}")
Expand Down Expand Up @@ -4044,11 +4049,10 @@ def _putimage(self, info):
# Palette
if info["cs"] == "Indexed":
self._newobj()
filter, pal = (
("/Filter /FlateDecode ", zlib.compress(info["pal"]))
if self.compress
else ("", info["pal"])
)
if self.compress:
filter, pal = ("/Filter /FlateDecode ", zlib.compress(info["pal"]))
else:
filter, pal = ("", info["pal"])
self._out(f"<<{filter}/Length {len(pal)}>>")
self._out(pdf_stream(pal))
self._out("endobj")
Expand Down
40 changes: 38 additions & 2 deletions fpdf/image_parsing.py
Expand Up @@ -56,15 +56,23 @@ def get_img_info(img, image_filter="AUTO", dims=None):
"""
if Image is None:
raise EnvironmentError("Pillow not available - fpdf2 cannot insert images")

if not isinstance(img, Image.Image):
img = Image.open(img)

if dims:
img = img.resize(dims, resample=RESAMPLE)

if image_filter == "AUTO":
# Very simple logic for now:
image_filter = "DCTDecode" if img.format == "JPEG" else "FlateDecode"
if img.mode not in ("L", "LA", "RGB", "RGBA"):

if img.mode in ("P", "PA") and image_filter != "FlateDecode":
img = img.convert("RGBA")

if img.mode not in ("L", "LA", "RGB", "RGBA", "P", "PA"):
img = img.convert("RGBA")

w, h = img.size
info = {}
if img.mode == "L":
Expand All @@ -79,6 +87,30 @@ def get_img_info(img, image_filter="AUTO", dims=None):
"JPXDecode",
):
info["smask"] = _to_data(img, image_filter, select_slice=alpha_channel)
elif img.mode == "P":
dpn, bpc, colspace = 1, 8, "Indexed"
info["data"] = _to_data(img, image_filter)
info["pal"] = img.palette.palette

# check if the P image has transparency
if img.info.get("transparency", None) is not None and image_filter not in (
"DCTDecode",
"JPXDecode",
):
# convert to RGBA to get the alpha channel for creating the smask
info["smask"] = _to_data(
img.convert("RGBA"), image_filter, select_slice=slice(3, None, 4)
)
elif img.mode == "PA":
dpn, bpc, colspace = 1, 8, "Indexed"
info["pal"] = img.palette.palette
alpha_channel = slice(1, None, 2)
info["data"] = _to_data(img, image_filter, remove_slice=alpha_channel)
if _has_alpha(img, alpha_channel) and image_filter not in (
"DCTDecode",
"JPXDecode",
):
info["smask"] = _to_data(img, image_filter, select_slice=alpha_channel)
elif img.mode == "RGB":
dpn, bpc, colspace = 3, 8, "DeviceRGB"
info["data"] = _to_data(img, image_filter)
Expand All @@ -102,7 +134,6 @@ def get_img_info(img, image_filter="AUTO", dims=None):
"bpc": bpc,
"f": image_filter,
"dp": dp,
"pal": "",
"trns": "",
}
)
Expand All @@ -113,18 +144,23 @@ def get_img_info(img, image_filter="AUTO", dims=None):
def _to_data(img, image_filter, **kwargs):
if image_filter == "FlateDecode":
return _to_zdata(img, **kwargs)

if img.mode == "LA":
img = img.convert("L")

if img.mode == "RGBA":
img = img.convert("RGB")

if image_filter == "DCTDecode":
compressed_bytes = BytesIO()
img.save(compressed_bytes, format="JPEG")
return compressed_bytes.getvalue()

if image_filter == "JPXDecode":
compressed_bytes = BytesIO()
img.save(compressed_bytes, format="JPEG2000")
return compressed_bytes.getvalue()

raise FPDFException(f'Unsupported image filter: "{image_filter}"')


Expand Down
Binary file added test/errors/flowers.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions test/errors/test_FPDF_errors.py
Expand Up @@ -4,6 +4,8 @@
import fpdf
import pytest
from fpdf.errors import FPDFException, FPDFUnicodeEncodingException
from fpdf.image_parsing import get_img_info
from PIL import Image

HERE = Path(__file__).resolve().parent

Expand Down Expand Up @@ -107,6 +109,13 @@ def test_repeated_calls_to_output(tmp_path):
assert_pdf_equal(pdf, HERE / "repeated_calls_to_output.pdf", tmp_path)


def test_unsupported_image_filter_error():
image_filter = "N/A"
with pytest.raises(FPDFException) as error:
get_img_info(img=Image.open(HERE / "flowers.png"), image_filter=image_filter)
assert str(error.value) == f'Unsupported image filter: "{image_filter}"'


def test_incorrent_number_of_pages_toc():
pdf = fpdf.FPDF()
pdf.add_page()
Expand Down
Binary file modified test/html/html_features.pdf
Binary file not shown.
Binary file modified test/html/html_images.pdf
Binary file not shown.
Binary file modified test/html/test_img_inside_html_table.pdf
Binary file not shown.
Binary file modified test/html/test_img_inside_html_table_centered.pdf
Binary file not shown.
Binary file modified test/html/test_img_inside_html_table_centered_with_align.pdf
Binary file not shown.
Binary file not shown.

0 comments on commit d2fa052

Please sign in to comment.