Skip to content

Commit e79eeff

Browse files
authored
Add cmyk support (#756)
1 parent c55f1e3 commit e79eeff

File tree

8 files changed

+31
-6
lines changed

8 files changed

+31
-6
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',
1818

1919
## [2.7.4] - Not released yet
2020
### Added
21+
- [`FPDF.image()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.image): CMYK images can now be inserted directly by passing them into the image method. Contributed by @devdev29
2122
- documentation on how to embed `graphs` and `charts` generated using `Pygal` lib: [documentation section](https://pyfpdf.github.io/fpdf2/Maths.html#using-pygal) - thanks to @ssavi-ict
2223
- documentation on how to use `fpdf2` with [FastAPI](https://fastapi.tiangolo.com/): <https://pyfpdf.github.io/fpdf2/UsageInWebAPI.html#FastAPI> - thanks to @KamarulAdha
2324
- [`FPDF.write_html()`](https://pyfpdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.write_html): `<table>` elements can now be aligned left or right on the page using `align=`

fpdf/image_parsing.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ def get_img_info(filename, img=None, image_filter="AUTO", dims=None):
115115
raise EnvironmentError("Pillow not available - fpdf2 cannot insert images")
116116

117117
is_pil_img = True
118+
jpeg_inverted = False # flag to check whether a cmyk image is jpeg or not, if set to True the decode array is inverted in output.py
118119
img_raw_data = None
119120
if not img or isinstance(img, (Path, str)):
120121
img_raw_data = load_image(filename)
@@ -146,7 +147,7 @@ def get_img_info(filename, img=None, image_filter="AUTO", dims=None):
146147
if img.mode in ("P", "PA") and image_filter != "FlateDecode":
147148
img = img.convert("RGBA")
148149

149-
if img.mode not in ("1", "L", "LA", "RGB", "RGBA", "P", "PA"):
150+
if img.mode not in ("1", "L", "LA", "RGB", "RGBA", "P", "PA", "CMYK"):
150151
img = img.convert("RGBA")
151152
img_altered = True
152153

@@ -161,7 +162,11 @@ def get_img_info(filename, img=None, image_filter="AUTO", dims=None):
161162
if img_raw_data is not None and not img_altered:
162163
# if we can use the original image bytes directly we do (JPEG and group4 TIFF only):
163164
if img.format == "JPEG" and image_filter == "DCTDecode":
164-
dpn, bpc, colspace = 3, 8, "DeviceRGB"
165+
if img.mode in ("RGB", "RGBA"):
166+
dpn, bpc, colspace = 3, 8, "DeviceRGB"
167+
elif img.mode == "CMYK":
168+
dpn, bpc, colspace = 4, 8, "DeviceCMYK"
169+
jpeg_inverted = True
165170
if img.mode == "L":
166171
dpn, bpc, colspace = 1, 8, "DeviceGray"
167172
img_raw_data.seek(0)
@@ -174,6 +179,7 @@ def get_img_info(filename, img=None, image_filter="AUTO", dims=None):
174179
"dpn": dpn,
175180
"bpc": bpc,
176181
"f": image_filter,
182+
"inverted": jpeg_inverted,
177183
"dp": f"/Predictor 15 /Colors {dpn} /Columns {w}",
178184
}
179185
# We can directly copy the data out of a CCITT Group 4 encoded TIFF, if it
@@ -218,6 +224,7 @@ def get_img_info(filename, img=None, image_filter="AUTO", dims=None):
218224
"cs": colspace,
219225
"bpc": bpc,
220226
"f": image_filter,
227+
"inverted": jpeg_inverted,
221228
"dp": f"/BlackIs1 {str(not inverted).lower()} /Columns {w} /K -1 /Rows {h}",
222229
}
223230

@@ -263,6 +270,9 @@ def get_img_info(filename, img=None, image_filter="AUTO", dims=None):
263270
"JPXDecode",
264271
):
265272
info["smask"] = _to_data(img, image_filter, select_slice=alpha_channel)
273+
elif img.mode == "CMYK":
274+
dpn, bpc, colspace = 4, 8, "DeviceCMYK"
275+
info["data"] = _to_data(img, image_filter)
266276
elif img.mode == "RGB":
267277
dpn, bpc, colspace = 3, 8, "DeviceRGB"
268278
info["data"] = _to_data(img, image_filter)
@@ -293,6 +303,7 @@ def get_img_info(filename, img=None, image_filter="AUTO", dims=None):
293303
"bpc": bpc,
294304
"dpn": dpn,
295305
"f": image_filter,
306+
"inverted": jpeg_inverted,
296307
"dp": dp,
297308
}
298309
)

fpdf/output.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -766,10 +766,8 @@ def _add_image(self, info):
766766
iccp_pdf_i = self._ensure_iccp(info)
767767
color_space = PDFArray(["/ICCBased", str(iccp_pdf_i), str("0"), "R"])
768768
elif color_space == "DeviceCMYK":
769-
decode = "[1 0 1 0 1 0 1 0]"
770-
raise NotImplementedError(
771-
"fpdf2 does not support DeviceCMYK ColorSpace yet - cf. issue #711"
772-
)
769+
if info["inverted"] is True:
770+
decode = "[1 0 1 0 1 0 1 0]"
773771

774772
decode_parms = f"<<{info['dp']} /BitsPerComponent {info['bpc']}>>"
775773
img_obj = PDFXObject(
79.5 KB
Binary file not shown.
16.3 KB
Binary file not shown.
15.2 KB
Loading
100 KB
Binary file not shown.

test/image/image_types/test_insert_images.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ def test_insert_jpg_flatedecode(tmp_path):
5050
assert_pdf_equal(pdf, HERE / "image_types_insert_jpg_flatedecode.pdf", tmp_path)
5151

5252

53+
def test_insert_jpg_cmyk(tmp_path):
54+
pdf = fpdf.FPDF()
55+
pdf.compress = False
56+
pdf.add_page()
57+
pdf.image(HERE / "insert_images_insert_jpg_cmyk.jpg", x=15, y=15)
58+
assert_pdf_equal(pdf, HERE / "images_types_insert_jpg_cmyk.pdf", tmp_path)
59+
60+
5361
def test_insert_png(tmp_path):
5462
pdf = fpdf.FPDF()
5563
pdf.add_page()
@@ -168,6 +176,13 @@ def test_insert_g4_tiff(tmp_path):
168176
assert_pdf_equal(pdf, HERE / "image_types_insert_tiff.pdf", tmp_path)
169177

170178

179+
def test_insert_tiff_cmyk(tmp_path):
180+
pdf = fpdf.FPDF()
181+
pdf.add_page()
182+
pdf.image(HERE / "insert_images_insert_tiff_cmyk.tiff", x=15, y=15)
183+
assert_pdf_equal(pdf, HERE / "image_types_insert_tiff_cmyk.pdf", tmp_path)
184+
185+
171186
def test_insert_pillow(tmp_path):
172187
pdf = fpdf.FPDF()
173188
pdf.add_page()

0 commit comments

Comments
 (0)