Skip to content

Commit 94d03e6

Browse files
committed
Allow users to set output intent
1 parent 5e31bba commit 94d03e6

File tree

14 files changed

+155
-164
lines changed

14 files changed

+155
-164
lines changed

tests/test_api.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -612,15 +612,21 @@ def test_partial_pdf_custom_metadata():
612612

613613

614614
@assert_no_logs
615-
def test_pdf_srgb():
616-
stdout = _run('--srgb --uncompressed-pdf - -', b'test')
615+
def test_output_intent():
616+
stdout = _run(
617+
'--uncompressed-pdf --output-intent="sRGB" - -',
618+
b'<div style="color: red">test')
617619
assert b'sRGB' in stdout
618620

619621

620622
@assert_no_logs
621-
def test_pdf_no_srgb():
622-
stdout = _run('--uncompressed-pdf - -', b'test')
623-
assert b'sRGB' not in stdout
623+
def test_output_intent_device_cmyk():
624+
stdout = _run('--uncompressed-pdf - -', b''.join((
625+
b'<style>@color-profile device-cmyk { src: url(',
626+
path2url(resource_path('cmyk.icc')).encode(),
627+
b'); components: c, m, y, k }</style>',
628+
b'<div style="color: device-cmyk(1 0 0 0)">test')))
629+
assert b'OutputIntents' in stdout
624630

625631

626632
@assert_no_logs

tests/test_pdf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -537,7 +537,7 @@ def test_embed_images_from_pages():
537537
document = Document(
538538
(page1, page2), metadata=DocumentMetadata(),
539539
font_config=FontConfiguration(), color_profiles={},
540-
url_fetcher=None).write_pdf()
540+
url_fetcher=None, output_intent=None).write_pdf()
541541
assert document.count(b'/Filter /DCTDecode') == 2
542542

543543

weasyprint/__init__.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,9 +46,8 @@
4646
#: Whether custom HTML metadata should be stored in the generated PDF.
4747
#: :param bool presentational_hints:
4848
#: Whether HTML presentational hints are followed.
49-
#: :param bool srgb:
50-
#: Whether sRGB color profile should be included and set as default for
51-
#: device-dependant RGB colors.
49+
#: :param string output_intent:
50+
#: CSS identifier or registered name of the output intent color space.
5251
#: :param bool optimize_images:
5352
#: Whether size of embedded images should be optimized, with no quality
5453
#: loss.
@@ -77,7 +76,7 @@
7776
'xmp_metadata': None,
7877
'custom_metadata': False,
7978
'presentational_hints': False,
80-
'srgb': False,
79+
'output_intent': None,
8180
'optimize_images': False,
8281
'jpeg_quality': None,
8382
'dpi': None,

weasyprint/__main__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,12 @@ def docstring(self):
114114
group.add_argument(
115115
'--custom-metadata', action='store_true',
116116
help='include custom HTML meta tags in PDF metadata')
117+
group.add_argument(
118+
'--output-intent',
119+
help='CSS identifier or registered name of the output intent color space')
117120
group.add_argument(
118121
'-p', '--presentational-hints', action='store_true',
119122
help='follow HTML presentational hints')
120-
group.add_argument('--srgb', action='store_true', help='include sRGB color profile')
121123
group.add_argument(
122124
'--optimize-images', action='store_true',
123125
help='optimize size of embedded images with no quality loss')

weasyprint/css/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1231,6 +1231,7 @@ def __init__(self, file_object, descriptors):
12311231
self.src = descriptors['src'][1]
12321232
self.renderingintent = descriptors['rendering-intent']
12331233
self.components = descriptors['components']
1234+
self.pdf_reference = None
12341235
self._profile = ImageCmsProfile(file_object)
12351236

12361237
@property

weasyprint/document.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,13 @@ def _render(cls, html, font_config, counter_style, color_profiles, options):
188188
if color_profiles is None:
189189
color_profiles = {}
190190

191+
# Set default PDF options for PDF variants.
192+
if variant := options['pdf_variant']:
193+
_, properties = VARIANTS[variant]
194+
for key, value in tuple(options.items()):
195+
if value is None and key in properties:
196+
options[key] = properties[key]
197+
191198
context = cls._build_layout_context(
192199
html, font_config, counter_style, color_profiles, options)
193200

@@ -200,11 +207,12 @@ def _render(cls, html, font_config, counter_style, color_profiles, options):
200207
rendering = cls(
201208
[Page(page_box) for page_box in page_boxes],
202209
DocumentMetadata(**get_html_metadata(html)),
203-
html.url_fetcher, font_config, color_profiles)
210+
html.url_fetcher, font_config, color_profiles, options['output_intent'])
204211
rendering._html = html
205212
return rendering
206213

207-
def __init__(self, pages, metadata, url_fetcher, font_config, color_profiles):
214+
def __init__(self, pages, metadata, url_fetcher, font_config, color_profiles,
215+
output_intent):
208216
#: A list of :class:`Page` objects.
209217
self.pages = pages
210218
#: A :class:`DocumentMetadata` object.
@@ -221,8 +229,10 @@ def __init__(self, pages, metadata, url_fetcher, font_config, color_profiles):
221229
# rendering is destroyed. This is needed as font_config.__del__ removes
222230
# fonts that may be used when rendering
223231
self.font_config = font_config
224-
232+
# List of color profiles.
225233
self.color_profiles = color_profiles
234+
# Output intent identifier.
235+
self.output_intent = output_intent
226236

227237
def copy(self, pages='all'):
228238
"""Take a subset of the pages.
@@ -255,7 +265,7 @@ def copy(self, pages='all'):
255265
pages = list(pages)
256266
return type(self)(
257267
pages, self.metadata, self.url_fetcher, self.font_config,
258-
self.color_profiles)
268+
self.color_profiles, self.output_intent)
259269

260270
def make_bookmark_tree(self, scale=1, transform_pages=False):
261271
"""Make a tree of all bookmarks in the document.
@@ -321,13 +331,12 @@ def write_pdf(self, target=None, zoom=1, finisher=None, **options):
321331
new_options.update(options)
322332
options = new_options
323333

324-
# Set default PDF version for PDF variants.
334+
# Set default PDF options for PDF variants.
325335
if variant := options['pdf_variant']:
326336
_, properties = VARIANTS[variant]
327-
if 'version' in properties and not options['pdf_version']:
328-
options['pdf_version'] = properties['version']
329-
if 'identifier' in properties and not options['pdf_identifier']:
330-
options['pdf_identifier'] = properties['identifier']
337+
for key, value in tuple(options.items()):
338+
if value is None and key in properties:
339+
options[key] = properties[key]
331340

332341
pdf = generate_pdf(self, target, zoom, **options)
333342

weasyprint/images.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -519,9 +519,7 @@ def draw(self, stream, concrete_width, concrete_height, style):
519519
for c0, c1, hint in color_couples)
520520
function = stream.create_stitching_function(
521521
domain, encode, bounds, sub_functions)
522-
# TODO: handle other color spaces.
523-
shading = stream.add_shading(
524-
shading_type, 'RGB', domain, points, extend, function)
522+
shading = stream.add_shading(shading_type, domain, points, extend, function)
525523
stream.transform(d=scale_y)
526524

527525
if any(alpha != 1 for alpha in alphas):
@@ -535,7 +533,7 @@ def draw(self, stream, concrete_width, concrete_height, style):
535533
function = stream.create_stitching_function(
536534
domain, encode, bounds, sub_functions)
537535
alpha_shading = alpha_stream.add_shading(
538-
shading_type, 'Gray', domain, points, extend, function)
536+
shading_type, domain, points, extend, function, 'DeviceGray')
539537
alpha_stream.transform(d=scale_y)
540538
alpha_stream.stream = [f'/{alpha_shading.id} sh']
541539

weasyprint/pdf/__init__.py

Lines changed: 34 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
"""PDF generation management."""
22

3-
from importlib.resources import files
4-
53
import pydyf
64
from tinycss2.color5 import D50, D65
75

@@ -125,13 +123,10 @@ def generate_pdf(document, target, zoom, **options):
125123
compress = not options['uncompressed_pdf']
126124

127125
# Set properties according to PDF variants
128-
srgb = options['srgb']
129126
pdf_tags = options['pdf_tags']
130127
variant = options['pdf_variant']
131128
if variant:
132129
variant_function, properties = VARIANTS[variant]
133-
if 'srgb' in properties:
134-
srgb = properties['srgb']
135130
if 'pdf_tags' in properties:
136131
pdf_tags = properties['pdf_tags']
137132

@@ -149,15 +144,15 @@ def generate_pdf(document, target, zoom, **options):
149144
})
150145
# Custom color profiles
151146
for key, color_profile in document.color_profiles.items():
152-
if key == 'device-cmyk':
153-
# Device CMYK profile is stored as OutputIntent.
154-
continue
155147
profile = pydyf.Stream(
156148
[color_profile.content],
157149
pydyf.Dictionary({'N': len(color_profile.components)}),
158150
compress=compress)
159151
pdf.add_object(profile)
152+
color_profile.pdf_reference = profile.reference
160153
color_space[key] = pydyf.Array(('/ICCBased', profile.reference))
154+
if key == 'default-cmyk':
155+
color_space['DefaultCMYK'] = pydyf.Array(('/ICCBased', profile.reference))
161156
pdf.add_object(color_space)
162157
resources = pydyf.Dictionary({
163158
'ExtGState': pydyf.Dictionary(),
@@ -195,7 +190,7 @@ def generate_pdf(document, target, zoom, **options):
195190
(right - left) / scale, (bottom - top) / scale)
196191
stream = Stream(
197192
document.fonts, page_rectangle, resources, images, tags,
198-
document.color_profiles, compress=compress)
193+
document.color_profiles, document.output_intent, compress=compress)
199194
stream.transform(d=-1, f=(page.height * scale))
200195
pdf.add_object(stream)
201196
page_streams.append(stream)
@@ -337,46 +332,41 @@ def generate_pdf(document, target, zoom, **options):
337332
pdf.catalog['Names'] = pydyf.Dictionary()
338333
pdf.catalog['Names']['Dests'] = dests
339334

340-
# Add output ICC profile.
341-
# TODO: we should allow the user or the PDF variant code to set custom values in
342-
# OutputIntents and remove the "srgb" option. See PDF 2.0 chapter 14.11.5, and
343-
# https://www.color.org/chardata/drsection1.xalter for a list of "standard
344-
# production conditions".
345-
if 'device-cmyk' in document.color_profiles:
346-
color_profile = document.color_profiles['device-cmyk']
347-
profile = pydyf.Stream(
348-
[color_profile.content], pydyf.Dictionary({'N': 4}), compress=compress)
349-
pdf.add_object(profile)
350-
pdf.catalog['OutputIntents'] = pydyf.Array([
351-
pydyf.Dictionary({
352-
'Type': '/OutputIntent',
353-
'S': '/GTS_PDFX',
354-
'OutputConditionIdentifier': pydyf.String(color_profile.name),
355-
'DestOutputProfile': profile.reference,
356-
}),
357-
])
358-
elif srgb:
359-
profile = pydyf.Stream(
360-
[(files(__package__) / 'sRGB2014.icc').read_bytes()],
361-
pydyf.Dictionary({'N': 3, 'Alternate': '/DeviceRGB'}),
362-
compress=compress)
363-
pdf.add_object(profile)
364-
pdf.catalog['OutputIntents'] = pydyf.Array([
365-
pydyf.Dictionary({
366-
'Type': '/OutputIntent',
367-
'S': '/GTS_PDFA1',
368-
'OutputConditionIdentifier': pydyf.String('sRGB IEC61966-2.1'),
369-
'DestOutputProfile': profile.reference,
370-
}),
371-
])
372-
373335
# Add tags
374336
if pdf_tags:
375337
add_tags(pdf, document, page_streams)
376338

339+
# Add output intents.
340+
subtype = '/GTS_PDFA1' if variant and 'pdf/a' in variant else '/GTS_PDFX'
341+
output_intent = document.output_intent
342+
color_profile = None
343+
if output_intent:
344+
if output_intent in document.color_profiles:
345+
color_profile = document.color_profile[document.output_intent]
346+
elif output_intent == 'device-cmyk':
347+
output_intent = 'CGATS TR 001'
348+
else:
349+
if 'device-cmyk' in document.color_profiles:
350+
color_profile = document.color_profiles['device-cmyk']
351+
elif document.color_profiles:
352+
color_profile = next(iter(document.color_profiles.values()))
353+
354+
# Add output intent
355+
if output_intent or color_profile:
356+
intents = pydyf.Dictionary({
357+
'Type': '/OutputIntent',
358+
'S': subtype,
359+
})
360+
if color_profile:
361+
intents['OutputConditionIdentifier'] = pydyf.String(color_profile.name)
362+
intents['Info'] = pydyf.String(color_profile.name)
363+
intents['DestOutputProfile'] = color_profile.pdf_reference
364+
else:
365+
intents['OutputConditionIdentifier'] = pydyf.String(output_intent)
366+
pdf.catalog['OutputIntents'] = pydyf.Array([intents])
367+
377368
# Apply PDF variants functions
378369
if variant:
379-
variant_function(
380-
pdf, metadata, document, page_streams, attachments, compress)
370+
variant_function(pdf, document, page_streams, attachments, compress)
381371

382372
return pdf

weasyprint/pdf/pdfa.py

Lines changed: 20 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,7 @@
55
import pydyf
66

77

8-
def pdfa(pdf, metadata, document, page_streams, attachments, compress,
9-
version, variant):
8+
def pdfa(pdf, document, page_streams, attachments, compress, version, variant):
109
"""Set metadata for PDF/A documents."""
1110

1211
# Handle attachments.
@@ -63,45 +62,30 @@ def pdfa(pdf, metadata, document, page_streams, attachments, compress,
6362
if version == 1:
6463
# Metadata compression is forbidden for version 1.
6564
compress = False
66-
metadata.include_in_pdf(pdf, 'a', version, variant, compress)
65+
document.metadata.include_in_pdf(pdf, 'a', version, variant, compress)
6766

6867
# Remove document information.
6968
if version >= 4:
7069
pdf.info.clear()
7170

7271

72+
def _values(version, pdf_tags=None):
73+
values = {'pdf_version': version, 'pdf_identifier': True, 'output_intent': 'sRGB'}
74+
if pdf_tags is not None:
75+
values['pdf_tags'] = pdf_tags
76+
return values
77+
78+
7379
VARIANTS = {
74-
'pdf/a-1b': (
75-
partial(pdfa, version=1, variant='B'),
76-
{'version': '1.4', 'identifier': True, 'srgb': True}),
77-
'pdf/a-2b': (
78-
partial(pdfa, version=2, variant='B'),
79-
{'version': '1.7', 'identifier': True, 'srgb': True}),
80-
'pdf/a-3b': (
81-
partial(pdfa, version=3, variant='B'),
82-
{'version': '1.7', 'identifier': True, 'srgb': True}),
83-
'pdf/a-2u': (
84-
partial(pdfa, version=2, variant='U'),
85-
{'version': '1.7', 'identifier': True, 'srgb': True}),
86-
'pdf/a-3u': (
87-
partial(pdfa, version=3, variant='U'),
88-
{'version': '1.7', 'identifier': True, 'srgb': True}),
89-
'pdf/a-4u': (
90-
partial(pdfa, version=4, variant='U'),
91-
{'version': '2.0', 'identifier': True, 'srgb': True}),
92-
'pdf/a-1a': (
93-
partial(pdfa, version=1, variant='A'),
94-
{'version': '1.4', 'identifier': True, 'srgb': True, 'pdf_tags': True}),
95-
'pdf/a-2a': (
96-
partial(pdfa, version=2, variant='A'),
97-
{'version': '1.7', 'identifier': True, 'srgb': True, 'pdf_tags': True}),
98-
'pdf/a-3a': (
99-
partial(pdfa, version=3, variant='A'),
100-
{'version': '1.7', 'identifier': True, 'srgb': True, 'pdf_tags': True}),
101-
'pdf/a-4e': (
102-
partial(pdfa, version=4, variant='E'),
103-
{'version': '2.0', 'identifier': True, 'srgb': True}),
104-
'pdf/a-4f': (
105-
partial(pdfa, version=4, variant='F'),
106-
{'version': '2.0', 'identifier': True, 'srgb': True}),
80+
'pdf/a-1b': (partial(pdfa, version=1, variant='B'), _values('1.4')),
81+
'pdf/a-2b': (partial(pdfa, version=2, variant='B'), _values('1.7')),
82+
'pdf/a-3b': (partial(pdfa, version=3, variant='B'), _values('1.7')),
83+
'pdf/a-2u': (partial(pdfa, version=2, variant='U'), _values('1.7')),
84+
'pdf/a-3u': (partial(pdfa, version=3, variant='U'), _values('1.7')),
85+
'pdf/a-4u': (partial(pdfa, version=4, variant='U'), _values('2.0')),
86+
'pdf/a-1a': (partial(pdfa, version=1, variant='A'), _values('1.4', pdf_tags=True)),
87+
'pdf/a-2a': (partial(pdfa, version=1, variant='A'), _values('1.7', pdf_tags=True)),
88+
'pdf/a-3a': (partial(pdfa, version=1, variant='A'), _values('1.7', pdf_tags=True)),
89+
'pdf/a-4e': (partial(pdfa, version=4, variant='U'), _values('2.0')),
90+
'pdf/a-4f': (partial(pdfa, version=4, variant='U'), _values('2.0')),
10791
}

weasyprint/pdf/pdfua.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@
33
from functools import partial
44

55

6-
def pdfua(pdf, metadata, document, page_streams, attachments, compress, version):
6+
def pdfua(pdf, document, page_streams, attachments, compress, version):
77
"""Set metadata for PDF/UA documents."""
88
# Common PDF metadata stream
9-
metadata.include_in_pdf(pdf, 'ua', version, conformance=None, compress=compress)
9+
document.metadata.include_in_pdf(
10+
pdf, 'ua', version, conformance=None, compress=compress)
1011

1112

1213
VARIANTS = {
13-
'pdf/ua-1': (partial(pdfua, version=1), {'version': '1.7', 'pdf_tags': True}),
14-
'pdf/ua-2': (partial(pdfua, version=2), {'version': '2.0', 'pdf_tags': True}),
14+
'pdf/ua-1': (partial(pdfua, version=1), {'pdf_version': '1.7', 'pdf_tags': True}),
15+
'pdf/ua-2': (partial(pdfua, version=2), {'pdf_version': '2.0', 'pdf_tags': True}),
1516
}

0 commit comments

Comments
 (0)