Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for embedding indexed images #443

Merged
merged 23 commits into from May 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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.