Skip to content

Commit

Permalink
Add OutputIntends and embed icc profile (#1369)
Browse files Browse the repository at this point in the history
* chore: Test of bidi cannot be done without ssl-certificate

* feat: output intents added

* feat: OutputIntents with ICC sRGB profile

* feat: dest_output_profile is now configurable

* feat: made OutputConditionIdentifier variable

* fix: enable tests with files from www.unicode.org

* fix: to avoid urlopen error [SSL: CERTIFICATE_VERIFY_FAILED] certificate verify failed: certificate has expired

* chore: blank lines added; let test_bidi as is

* chore: let test_bidi as is

* chore: added documentation for OutputIntends

* chore: refactoring for pull request

* chore: refactoring for PR

* chore: removed merge conflict

* chore: commented out unnessessary function

* chore: black messages and operand | fixed

* chore: for the linters

* chore: copy of icc profile added

* chore: example modified, icc profile copy removed from docs

* chore: black error removed, test without_OutputIntents added

* chore: use Objects directly

---------

Co-authored-by: Herbert Lischka <[email protected]>
  • Loading branch information
lka and lka-berlin authored Feb 26, 2025
1 parent 3fb6d9e commit dfcc438
Show file tree
Hide file tree
Showing 12 changed files with 390 additions and 2 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ This can also be enabled programmatically with `warnings.simplefilter('default',

## [2.8.3] - Not released yet
### Added
* support for [Output Intends](https://py-pdf.github.io/fpdf2/Images.html#output-intends) on document level
* support for [shading patterns (gradients)](https://py-pdf.github.io/fpdf2/Patterns.html) - thanks to @andersonhc - [PR #1334](https://github.com/py-pdf/fpdf2/pull/1334)
* support for [setting a minimal row height in tables](https://py-pdf.github.io/fpdf2/Tables.html#setting-row-height)
* support for [`v_align` at the row level in tables](https://py-pdf.github.io/fpdf2/Tables.html#setting-vertical-alignment-of-text-in-cells)
Expand Down
38 changes: 38 additions & 0 deletions docs/Images.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,44 @@ Beware that "flattening" images into JPEGs this way will fill transparent areas
The allowed `image_filter` values are listed in the [image_parsing]( https://github.com/py-pdf/fpdf2/blob/master/fpdf/image_parsing.py) module and are currently:
`FlateDecode` (lossless zlib/deflate compression), `DCTDecode` (lossy compression with JPEG) and `JPXDecode` (lossy compression with JPEG2000).

## Output Intends ##

> Output Intends [allow] the contents of referenced icc profiles to be embedded directly within the body of the PDF file. This makes the PDF file a self-contained unit that can be stored or transmitted as a single entity.
### Add Desired Output Intent to the Output Intents Array ###
`fpdf2` gives access to this feature through the method [`set_output_intent()`](https://py-pdf.github.io/fpdf2/fpdf/fpdf.html#fpdf.fpdf.FPDF.set_output_intent):

#### Specify ICCProfile Stream ####
[`ICCProfileStreamDict`](https://py-pdf.github.io/fpdf2/fpdf/output.html#fpdf.output.output.ICCProfileStreamDict) Class is needed to specify the file object of the referenced icc profile.

#### Example: ####
```python
from pathlib import Path
from fpdf import FPDF
from fpdf.enums import OutputIntentSubType
from fpdf.output import PDFICCProfileObject

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

pdf = FPDF()

with open(HERE / "sRGB2014.icc", "rb") as iccp_file:
icc_profile = PDFICCProfileObject(
contents=iccp_file.read(), n=3, alternate="DeviceRGB"
)

pdf.set_output_intent(
OutputIntentSubType.PDFA,
"sRGB",
'IEC 61966-2-1:1999',
"http://www.color.org",
iccp_file,
"sRGB2014 (v2)",
)
```

The needed profiles and descriptions can be found at [International Color Consortium](https://color.org/).

## ICC Profiles

The ICC profile of the included images are read through the PIL function `Image.info.get("icc_profile)"` and are included in the PDF as objects.
Expand Down
13 changes: 13 additions & 0 deletions fpdf/enums.py
Original file line number Diff line number Diff line change
Expand Up @@ -1024,6 +1024,19 @@ class TextDirection(CoerciveEnum):
"bottom to top"


class OutputIntentSubType(CoerciveEnum):
"Definition for Output Intent Subtypes"

PDFX = Name("GTS_PDFX")
"PDF/X-1a which is based upon CMYK processing"

PDFA = Name("GTS_PDFA1")
"PDF/A (ISO 19005) standard to produce RGB output"

ISOPDF = Name("ISO_PDFE1")
"ISO_PDFE1 PDF/E standards (ISO 24517, all parts)"


class PageLabelStyle(CoerciveEnum):
"Style of the page label"

Expand Down
55 changes: 55 additions & 0 deletions fpdf/fpdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ class Image:
WrapMode,
XPos,
YPos,
OutputIntentSubType,
)
from .errors import FPDFException, FPDFPageFormatException, FPDFUnicodeEncodingException
from .fonts import CoreFont, CORE_FONTS, FontFace, TextStyle, TitleStyle, TTFFont
Expand Down Expand Up @@ -126,6 +127,8 @@ class Image:
PDFPageLabel,
ResourceCatalog,
stream_content_for_raster_image,
PDFICCProfileObject,
OutputIntentDictionary,
)
from .recorder import FPDFRecorder
from .sign import Signature
Expand Down Expand Up @@ -301,13 +304,16 @@ def __init__(
self.oversized_images = None
self.oversized_images_ratio = 2 # number of pixels per UserSpace point
self.struct_builder = StructureTreeBuilder()

self.toc_placeholder = None # optional ToCPlaceholder instance
self._outline: list[OutlineSection] = [] # list of OutlineSection
# flag set true while rendering the table of contents
self.in_toc_rendering = False
# allow page insertion when writing the table of contents
self._toc_allow_page_insertion = False
self._toc_inserted_pages = 0 # number of pages inserted
self._output_intents = [] # optional list of Output Intents

self._sign_key = None
self.title = None
self.section_title_styles = {} # level -> TextStyle
Expand Down Expand Up @@ -469,6 +475,55 @@ def page_mode(self, page_mode):
elif self._page_mode == PageMode.USE_OC:
self._set_min_pdf_version("1.5")

@property
def output_intents(self):
return self._output_intents

# @output_intents.setter
def set_output_intent(
self,
subtype: OutputIntentSubType,
output_condition_identifier: str = None,
output_condition: str = None,
registry_name: str = None,
dest_output_profile: PDFICCProfileObject = None,
info: str = None,
):
"""
Adds desired Output Intent to the Output Intents array:
Args:
subtype (OutputIntentSubType, required): PDFA, PDFX or ISOPDF
output_condition_identifier (str, required): see the Name in
https://www.color.org/registry.xalter
output_condition (str, optional): see the Definition in
https://www.color.org/registry.xalter
registry_name (str, optional): "https://www.color.org"
dest_output_profile (PDFICCProfileObject, required/optional):
PDFICCProfileObject | None # (required if
output_condition_identifier does not specify a standard
production condition; optional otherwise)
info (str, required/optional see dest_output_profile): human
readable description of profile
"""
subtypes_in_arr = [_.s for _ in self.output_intents]
if subtype.value not in subtypes_in_arr:
outputIntent = OutputIntentDictionary(
subtype,
output_condition_identifier,
output_condition,
registry_name,
dest_output_profile,
info,
)
self._output_intents.append(outputIntent)
else:
raise ValueError(
"set_output_intent: subtype '" + subtype.value + "' already exists."
)
if self.output_intents:
self._set_min_pdf_version("1.4")

@property
def epw(self):
"""
Expand Down
93 changes: 91 additions & 2 deletions fpdf/output.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

from .annotations import PDFAnnotation
from .enums import PDFResourceType, PageLabelStyle, SignatureFlag
from .enums import OutputIntentSubType
from .errors import FPDFException
from .line_break import TotalPagesSubstitutionFragment
from .image_datastructures import RasterImageInfo
Expand Down Expand Up @@ -145,6 +146,7 @@ def __init__(
self.metadata = None
self.names = None
self.outlines = None
self.output_intents = None
self.struct_tree_root = None


Expand Down Expand Up @@ -214,7 +216,14 @@ def __init__(
self.s_mask = None


class PDFICCPObject(PDFContentStream):
class PDFICCProfileObject(PDFContentStream):
"""holds values for ICC Profile Stream
Args:
contents (str): stream content
n (int): [1|3|4], # the numbers for colors 1=Gray, 3=RGB, 4=CMYK
alternate (str): ['DeviceGray'|'DeviceRGB'|'DeviceCMYK']
"""

__slots__ = ( # RAM usage optimization
"_id",
"_contents",
Expand Down Expand Up @@ -441,6 +450,74 @@ def serialize(self, _security_handler=None):
return "\n".join(out)


class OutputIntentDictionary:
"""The optional OutputIntents (PDF 1.4) entry in the document
catalog dictionary holds an array of output intent dictionaries,
each describing the colour reproduction characteristics of a possible
output device.
Args:
subtype (OutputIntentSubType, required): PDFA, PDFX or ISOPDF
output_condition_identifier (str, required): see the Name in
https://www.color.org/registry.xalter
output_condition (str, optional): see the Definition in
https://www.color.org/registry.xalter
registry_name (str, optional): "https://www.color.org"
dest_output_profile (PDFICCProfileObject, required/optional):
PDFICCProfileObject | None # (required if
output_condition_identifier does not specify a standard
production condition; optional otherwise)
info (str, required/optional see dest_output_profile): human
readable description of profile
"""

__slots__ = ( # RAM usage optimization
"type",
"s",
"output_condition_identifier",
"output_condition",
"registry_name",
"dest_output_profile",
"info",
)

def __init__(
self,
subtype: "OutputIntentSubType | str",
output_condition_identifier: str,
output_condition: str = None,
registry_name: str = None,
dest_output_profile: PDFICCProfileObject = None,
info: str = None,
):
self.type = Name("OutputIntent")
self.s = Name(OutputIntentSubType.coerce(subtype).value)
self.output_condition_identifier = (
PDFString(output_condition_identifier)
if output_condition_identifier
else None
)
self.output_condition = (
PDFString(output_condition) if output_condition else None
)
self.registry_name = PDFString(registry_name) if registry_name else None
self.dest_output_profile = (
dest_output_profile
if dest_output_profile
and isinstance(dest_output_profile, PDFICCProfileObject)
else None
)
self.info = PDFString(info) if info else None

def serialize(self, _security_handler=None, _obj_id=None):
obj_dict = build_obj_dict(
{key: getattr(self, key) for key in dir(self)},
_security_handler=_security_handler,
_obj_id=_obj_id,
)
return pdf_dict(obj_dict)


class ResourceCatalog:
"Manage the indexing of resources and association to the pages they are used"

Expand Down Expand Up @@ -530,6 +607,7 @@ def bufferize(self):
xmp_metadata_obj = self._add_xmp_metadata()
info_obj = self._add_info()
encryption_obj = self._add_encryption()

xref = PDFXrefAndTrailer(self)
self.pdf_objs.append(xref)

Expand Down Expand Up @@ -883,7 +961,7 @@ def _ensure_iccp(self, img_info):
break
assert iccp_content is not None
# Note: n should be 4 if the profile ColorSpace is CMYK
iccp_obj = PDFICCPObject(
iccp_obj = PDFICCProfileObject(
contents=iccp_content, n=img_info["dpn"], alternate=img_info["cs"]
)
iccp_pdf_i = self._add_pdf_obj(iccp_obj, "iccp")
Expand Down Expand Up @@ -1159,6 +1237,15 @@ def _add_encryption(self):
return pdf_obj
return None

def _add_output_intents(self):
"""should be added in _add_catalog"""
if not self.fpdf.output_intents:
return None
for item in self.fpdf.output_intents:
if item.dest_output_profile:
self._add_pdf_obj(item.dest_output_profile)
return PDFArray(self.fpdf.output_intents)

def _add_catalog(self):
fpdf = self.fpdf
catalog_obj = PDFCatalog(
Expand All @@ -1167,6 +1254,8 @@ def _add_catalog(self):
page_mode=fpdf.page_mode,
viewer_preferences=fpdf.viewer_preferences,
)
catalog_obj.output_intents = self._add_output_intents()

self._add_pdf_obj(catalog_obj)
return catalog_obj

Expand Down
Empty file added test/output_intents/__init__.py
Empty file.
Binary file added test/output_intents/sRGB2014.icc
Binary file not shown.
Loading

0 comments on commit dfcc438

Please sign in to comment.