From ade987e1501e14a5a69e30f8161be386e0c8379f Mon Sep 17 00:00:00 2001 From: Taylor Smith Date: Thu, 12 Nov 2026 10:03:31 -0600 Subject: [PATCH 1/3] client: Add color conversion rendering intents - Expose the documented rendering-intent choices as a public type. - Send an explicitly selected intent through sync and async clients while preserving the existing omitted-parameter behavior. Assisted-by: Codex --- src/pdfrest/client.py | 9 +++++++++ src/pdfrest/models/_internal.py | 5 +++++ src/pdfrest/types/__init__.py | 2 ++ src/pdfrest/types/public.py | 22 ++++++++++++++++++++++ 4 files changed, 38 insertions(+) diff --git a/src/pdfrest/client.py b/src/pdfrest/client.py index 2ea5083..655cb6b 100644 --- a/src/pdfrest/client.py +++ b/src/pdfrest/client.py @@ -175,6 +175,7 @@ PdfPageSize, PdfPresetColorProfile, PdfRedactionInstruction, + PdfRenderingIntent, PdfRestriction, PdfRGBColor, PdfSignatureConfiguration, @@ -5047,6 +5048,7 @@ def convert_colors( *, color_profile: PdfPresetColorProfile | PdfRestFile | Sequence[PdfRestFile], preserve_black: bool = False, + rendering_intent: PdfRenderingIntent | None = None, output: str | None = None, extra_query: Query | None = None, extra_headers: AnyMapping | None = None, @@ -5061,6 +5063,7 @@ def convert_colors( file: Uploaded input file or files as `PdfRestFile` objects. color_profile: Named color profile used by color conversion. preserve_black: When true, keeps pure black content from being remapped during color-profile conversion. + rendering_intent: Color-mapping strategy used when converting colors to the selected ICC profile. When omitted, pdfRest uses the profile-defined intent. output: Output filename prefix used by pdfRest when creating files. extra_query: Additional query parameters merged into the request. extra_headers: Additional HTTP headers merged into the request. @@ -5079,6 +5082,8 @@ def convert_colors( "color_profile": color_profile, "preserve_black": preserve_black, } + if rendering_intent is not None: + payload["rendering_intent"] = rendering_intent if output is not None: payload["output"] = output @@ -8928,6 +8933,7 @@ async def convert_colors( *, color_profile: PdfPresetColorProfile | PdfRestFile | Sequence[PdfRestFile], preserve_black: bool = False, + rendering_intent: PdfRenderingIntent | None = None, output: str | None = None, extra_query: Query | None = None, extra_headers: AnyMapping | None = None, @@ -8942,6 +8948,7 @@ async def convert_colors( file: Uploaded input file or files as `PdfRestFile` objects. color_profile: Named color profile used by color conversion. preserve_black: When true, keeps pure black content from being remapped during color-profile conversion. + rendering_intent: Color-mapping strategy used when converting colors to the selected ICC profile. When omitted, pdfRest uses the profile-defined intent. output: Output filename prefix used by pdfRest when creating files. extra_query: Additional query parameters merged into the request. extra_headers: Additional HTTP headers merged into the request. @@ -8960,6 +8967,8 @@ async def convert_colors( "color_profile": color_profile, "preserve_black": preserve_black, } + if rendering_intent is not None: + payload["rendering_intent"] = rendering_intent if output is not None: payload["output"] = output diff --git a/src/pdfrest/models/_internal.py b/src/pdfrest/models/_internal.py index c6307a2..7b9a067 100644 --- a/src/pdfrest/models/_internal.py +++ b/src/pdfrest/models/_internal.py @@ -41,6 +41,7 @@ PdfPageOrientation, PdfPageSize, PdfPresetColorProfile, + PdfRenderingIntent, PdfRestriction, PdfStructuredTextDataPresentation, PdfStructuredTextFontName, @@ -3870,6 +3871,10 @@ class PdfConvertColorsPayload(BaseModel): Field(serialization_alias="preserve_black", default=None), BeforeValidator(_bool_to_true_false), ] = None + rendering_intent: Annotated[ + PdfRenderingIntent | None, + Field(serialization_alias="rendering_intent", default=None), + ] = None output: Annotated[ str | None, Field(serialization_alias="output", min_length=1, default=None), diff --git a/src/pdfrest/types/__init__.py b/src/pdfrest/types/__init__.py index 0c02184..8f30fbc 100644 --- a/src/pdfrest/types/__init__.py +++ b/src/pdfrest/types/__init__.py @@ -46,6 +46,7 @@ PdfRedactionInstruction, PdfRedactionPreset, PdfRedactionType, + PdfRenderingIntent, PdfRestriction, PdfRGBColor, PdfSignatureConfiguration, @@ -135,6 +136,7 @@ "PdfRedactionInstruction", "PdfRedactionPreset", "PdfRedactionType", + "PdfRenderingIntent", "PdfRestriction", "PdfSignatureConfiguration", "PdfSignatureCredentials", diff --git a/src/pdfrest/types/public.py b/src/pdfrest/types/public.py index 934af78..4ee0491 100644 --- a/src/pdfrest/types/public.py +++ b/src/pdfrest/types/public.py @@ -59,6 +59,7 @@ "PdfRedactionInstruction", "PdfRedactionPreset", "PdfRedactionType", + "PdfRenderingIntent", "PdfRestriction", "PdfSignatureConfiguration", "PdfSignatureCredentials", @@ -1324,5 +1325,26 @@ class PdfPemCredentials(TypedDict): ] PdfColorProfile = PdfPresetColorProfile + +PdfRenderingIntent: TypeAlias = Literal[ + "profile", + "perceptual", + "relative_colorimetric", + "saturation", + "absolute_colorimetric", +] +"""Color-mapping strategy used when converting PDF colors. + +Accepted values: + +- `profile`: Use the rendering intent specified by the selected ICC profile. +- `perceptual`: Compress the source gamut to preserve visual color relationships. +- `relative_colorimetric`: Preserve in-gamut colors relative to the destination + white point and map out-of-gamut colors to the closest reproducible colors. +- `saturation`: Prioritize vivid color saturation over exact color relationships. +- `absolute_colorimetric`: Preserve the source white point and reproduce in-gamut + colors without adapting them to the destination white point. +""" + WatermarkHorizontalAlignment = Literal["left", "center", "right"] WatermarkVerticalAlignment = Literal["top", "center", "bottom"] From 8aba7667b31944a3e86a6638675a17c135fb6330 Mon Sep 17 00:00:00 2001 From: Taylor Smith Date: Thu, 12 Nov 2026 10:03:58 -0600 Subject: [PATCH 2/3] tests: Cover color conversion rendering intents - Verify every documented intent through payload, sync, and async paths. - Add live success and rejection coverage for the server contract. Assisted-by: Codex --- tests/live/test_live_convert_colors.py | 110 +++++++++++++- tests/test_convert_colors.py | 198 ++++++++++++++++++++++++- 2 files changed, 306 insertions(+), 2 deletions(-) diff --git a/tests/live/test_live_convert_colors.py b/tests/live/test_live_convert_colors.py index 9f9ebb2..d0569c5 100644 --- a/tests/live/test_live_convert_colors.py +++ b/tests/live/test_live_convert_colors.py @@ -6,7 +6,7 @@ from pdfrest import AsyncPdfRestClient, PdfRestApiError, PdfRestClient from pdfrest.models import PdfRestFile -from pdfrest.types import PdfPresetColorProfile +from pdfrest.types import PdfPresetColorProfile, PdfRenderingIntent from ..resources import get_test_resource_path @@ -14,6 +14,10 @@ tuple[PdfPresetColorProfile, ...], get_args(PdfPresetColorProfile), ) +ALL_RENDERING_INTENTS: tuple[PdfRenderingIntent, ...] = cast( + tuple[PdfRenderingIntent, ...], + get_args(PdfRenderingIntent), +) @pytest.fixture(scope="module") @@ -72,6 +76,39 @@ def test_live_convert_colors_color_profiles_success( assert str(response.input_id) == str(uploaded_pdf_for_color_conversion.id) +@pytest.mark.parametrize( + "rendering_intent", + [ + pytest.param(rendering_intent, id=f"rendering-intent-{rendering_intent}") + for rendering_intent in ALL_RENDERING_INTENTS + ], +) +def test_live_convert_colors_rendering_intents_success( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_for_color_conversion: PdfRestFile, + rendering_intent: PdfRenderingIntent, +) -> None: + with PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = client.convert_colors( + uploaded_pdf_for_color_conversion, + color_profile="srgb", + rendering_intent=rendering_intent, + ) + + assert len(response.output_files) == 1 + output_file = response.output_file + assert output_file.name.endswith(".pdf") + assert output_file.type == "application/pdf" + assert output_file.size > 0 + assert str(output_file.id) in str(output_file.url) + assert response.warning is None + assert str(response.input_id) == str(uploaded_pdf_for_color_conversion.id) + + def test_live_convert_colors_custom_profile_success( pdfrest_api_key: str, pdfrest_live_base_url: str, @@ -165,6 +202,40 @@ async def test_live_async_convert_colors_color_profiles_success( assert str(response.input_id) == str(uploaded_pdf_for_color_conversion.id) +@pytest.mark.asyncio +@pytest.mark.parametrize( + "rendering_intent", + [ + pytest.param(rendering_intent, id=f"rendering-intent-{rendering_intent}") + for rendering_intent in ALL_RENDERING_INTENTS + ], +) +async def test_live_async_convert_colors_rendering_intents_success( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_for_color_conversion: PdfRestFile, + rendering_intent: PdfRenderingIntent, +) -> None: + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + response = await client.convert_colors( + uploaded_pdf_for_color_conversion, + color_profile="srgb", + rendering_intent=rendering_intent, + ) + + assert len(response.output_files) == 1 + output_file = response.output_file + assert output_file.name.endswith(".pdf") + assert output_file.type == "application/pdf" + assert output_file.size > 0 + assert str(output_file.id) in str(output_file.url) + assert response.warning is None + assert str(response.input_id) == str(uploaded_pdf_for_color_conversion.id) + + @pytest.mark.asyncio async def test_live_async_convert_colors_custom_profile_success( pdfrest_api_key: str, @@ -255,6 +326,43 @@ async def test_live_async_convert_colors_invalid_color_profile( ) +def test_live_convert_colors_invalid_rendering_intent( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_for_color_conversion: PdfRestFile, +) -> None: + with ( + PdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client, + pytest.raises(PdfRestApiError, match=r"(?i)(rendering|intent)"), + ): + client.convert_colors( + uploaded_pdf_for_color_conversion, + color_profile="srgb", + extra_body={"rendering_intent": "not-an-intent"}, + ) + + +@pytest.mark.asyncio +async def test_live_async_convert_colors_invalid_rendering_intent( + pdfrest_api_key: str, + pdfrest_live_base_url: str, + uploaded_pdf_for_color_conversion: PdfRestFile, +) -> None: + async with AsyncPdfRestClient( + api_key=pdfrest_api_key, + base_url=pdfrest_live_base_url, + ) as client: + with pytest.raises(PdfRestApiError, match=r"(?i)(rendering|intent)"): + await client.convert_colors( + uploaded_pdf_for_color_conversion, + color_profile="srgb", + extra_body={"rendering_intent": "not-an-intent"}, + ) + + def test_live_convert_colors_invalid_file_id( pdfrest_api_key: str, pdfrest_live_base_url: str, diff --git a/tests/test_convert_colors.py b/tests/test_convert_colors.py index c93c680..46727a7 100644 --- a/tests/test_convert_colors.py +++ b/tests/test_convert_colors.py @@ -10,7 +10,7 @@ from pdfrest import AsyncPdfRestClient, PdfRestClient from pdfrest.models import PdfRestFile, PdfRestFileBasedResponse, PdfRestFileID from pdfrest.models._internal import PdfConvertColorsPayload -from pdfrest.types import PdfPresetColorProfile +from pdfrest.types import PdfPresetColorProfile, PdfRenderingIntent from .graphics_test_helpers import ( ASYNC_API_KEY, @@ -34,6 +34,42 @@ def _make_icc_file() -> PdfRestFile: tuple[PdfPresetColorProfile, ...], get_args(PdfPresetColorProfile), ) +ALL_RENDERING_INTENTS: tuple[PdfRenderingIntent, ...] = cast( + tuple[PdfRenderingIntent, ...], + get_args(PdfRenderingIntent), +) + + +@pytest.mark.parametrize( + "rendering_intent", + [ + pytest.param(rendering_intent, id=f"rendering-intent-{rendering_intent}") + for rendering_intent in ALL_RENDERING_INTENTS + ], +) +def test_convert_colors_payload_rendering_intent_serialization( + rendering_intent: PdfRenderingIntent, +) -> None: + input_file = make_pdf_file(PdfRestFileID.generate(1)) + + payload = PdfConvertColorsPayload.model_validate( + { + "files": input_file, + "color_profile": "srgb", + "rendering_intent": rendering_intent, + } + ) + + assert payload.model_dump( + mode="json", + by_alias=True, + exclude_none=True, + exclude_unset=True, + ) == { + "id": str(input_file.id), + "color_profile": "srgb", + "rendering_intent": rendering_intent, + } @pytest.mark.parametrize( @@ -113,6 +149,63 @@ def handler(request: httpx.Request) -> httpx.Response: assert str(response.input_id) == str(input_file.id) +@pytest.mark.parametrize( + "rendering_intent", + [ + pytest.param(rendering_intent, id=f"rendering-intent-{rendering_intent}") + for rendering_intent in ALL_RENDERING_INTENTS + ], +) +def test_convert_colors_rendering_intents( + monkeypatch: pytest.MonkeyPatch, + rendering_intent: PdfRenderingIntent, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(1)) + output_id = str(PdfRestFileID.generate()) + seen = {"post": 0, "get": 0} + + def handler(request: httpx.Request) -> httpx.Response: + if ( + request.method == "POST" + and request.url.path == "/pdf-with-converted-colors" + ): + seen["post"] += 1 + assert json.loads(request.content.decode("utf-8")) == { + "id": str(input_file.id), + "color_profile": "srgb", + "preserve_black": "false", + "rendering_intent": rendering_intent, + } + return httpx.Response( + 200, + json={"inputId": [input_file.id], "outputId": [output_id]}, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + seen["get"] += 1 + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "rendering-intent.pdf", + "application/pdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + with PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client: + response = client.convert_colors( + input_file, + color_profile="srgb", + rendering_intent=rendering_intent, + ) + + assert seen == {"post": 1, "get": 1} + assert response.output_file.name == "rendering-intent.pdf" + + def test_convert_colors_request_customization( monkeypatch: pytest.MonkeyPatch, ) -> None: @@ -135,6 +228,7 @@ def handler(request: httpx.Request) -> httpx.Response: assert payload["color_profile"] == "custom" assert payload["profile_id"] == str(profile_file.id) assert payload["preserve_black"] == "true" + assert payload["rendering_intent"] == "perceptual" return httpx.Response( 200, json={ @@ -163,6 +257,7 @@ def handler(request: httpx.Request) -> httpx.Response: input_file, color_profile=profile_file, preserve_black=True, + rendering_intent="perceptual", output="custom", extra_query={"trace": "true"}, extra_headers={"X-Debug": "sync"}, @@ -254,6 +349,64 @@ def handler(request: httpx.Request) -> httpx.Response: assert str(response.input_id) == str(input_file.id) +@pytest.mark.parametrize( + "rendering_intent", + [ + pytest.param(rendering_intent, id=f"rendering-intent-{rendering_intent}") + for rendering_intent in ALL_RENDERING_INTENTS + ], +) +@pytest.mark.asyncio +async def test_async_convert_colors_rendering_intents( + monkeypatch: pytest.MonkeyPatch, + rendering_intent: PdfRenderingIntent, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + input_file = make_pdf_file(PdfRestFileID.generate(2)) + output_id = str(PdfRestFileID.generate()) + seen = {"post": 0, "get": 0} + + def handler(request: httpx.Request) -> httpx.Response: + if ( + request.method == "POST" + and request.url.path == "/pdf-with-converted-colors" + ): + seen["post"] += 1 + assert json.loads(request.content.decode("utf-8")) == { + "id": str(input_file.id), + "color_profile": "srgb", + "preserve_black": "false", + "rendering_intent": rendering_intent, + } + return httpx.Response( + 200, + json={"inputId": [input_file.id], "outputId": [output_id]}, + ) + if request.method == "GET" and request.url.path == f"/resource/{output_id}": + seen["get"] += 1 + return httpx.Response( + 200, + json=build_file_info_payload( + output_id, + "async-rendering-intent.pdf", + "application/pdf", + ), + ) + msg = f"Unexpected request {request.method} {request.url}" + raise AssertionError(msg) + + transport = httpx.MockTransport(handler) + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + response = await client.convert_colors( + input_file, + color_profile="srgb", + rendering_intent=rendering_intent, + ) + + assert seen == {"post": 1, "get": 1} + assert response.output_file.name == "async-rendering-intent.pdf" + + @pytest.mark.asyncio async def test_async_convert_colors_request_customization( monkeypatch: pytest.MonkeyPatch, @@ -277,6 +430,7 @@ def handler(request: httpx.Request) -> httpx.Response: assert payload["color_profile"] == "custom" assert payload["profile_id"] == str(profile_file.id) assert payload["preserve_black"] == "false" + assert payload["rendering_intent"] == "relative_colorimetric" assert payload["output"] == "async-custom" return httpx.Response( 200, @@ -305,6 +459,7 @@ def handler(request: httpx.Request) -> httpx.Response: response = await client.convert_colors( input_file, color_profile=profile_file, + rendering_intent="relative_colorimetric", output="async-custom", extra_query={"trace": "async"}, extra_headers={"X-Debug": "async"}, @@ -498,3 +653,44 @@ async def test_async_convert_colors_rejects_invalid_color_profile( async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: with pytest.raises(ValidationError, match="Input should be"): await client.convert_colors(pdf_file, color_profile=invalid_color_profile) + + +def test_convert_colors_rejects_invalid_rendering_intent( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + pdf_file = make_pdf_file(PdfRestFileID.generate(1)) + transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError)) + + with ( + PdfRestClient(api_key=VALID_API_KEY, transport=transport) as client, + pytest.raises( + ValidationError, + match=r"(?s)rendering_intent.*Input should be", + ), + ): + client.convert_colors( + pdf_file, + color_profile="srgb", + rendering_intent=cast(Any, "not-an-intent"), + ) + + +@pytest.mark.asyncio +async def test_async_convert_colors_rejects_invalid_rendering_intent( + monkeypatch: pytest.MonkeyPatch, +) -> None: + monkeypatch.delenv("PDFREST_API_KEY", raising=False) + pdf_file = make_pdf_file(PdfRestFileID.generate(2)) + transport = httpx.MockTransport(lambda request: (_ for _ in ()).throw(RuntimeError)) + + async with AsyncPdfRestClient(api_key=ASYNC_API_KEY, transport=transport) as client: + with pytest.raises( + ValidationError, + match=r"(?s)rendering_intent.*Input should be", + ): + await client.convert_colors( + pdf_file, + color_profile="srgb", + rendering_intent=cast(Any, "not-an-intent"), + ) From 22af602c754ba009224a738da2c41fbadef165c4 Mon Sep 17 00:00:00 2001 From: Taylor Smith Date: Thu, 12 Nov 2026 10:04:27 -0600 Subject: [PATCH 3/3] examples: Demonstrate color conversion rendering intent - Add a standalone sRGB conversion example using perceptual rendering. - Link the rendering-intent contract from the API guide and inventory. Assisted-by: Codex --- docs/api-guide.md | 5 +- examples/README.md | 2 + .../convert_pdf_colors_example.py | 55 +++++++++++++++++++ 3 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 examples/convert_colors/convert_pdf_colors_example.py diff --git a/docs/api-guide.md b/docs/api-guide.md index 6e459ef..9126b7c 100644 --- a/docs/api-guide.md +++ b/docs/api-guide.md @@ -140,7 +140,10 @@ Use this group to improve compatibility, performance, or file size. [flatten_transparencies][pdfrest.PdfRestClient.flatten_transparencies] - Raster and color normalization: [rasterize_pdf][pdfrest.PdfRestClient.rasterize_pdf], - [convert_colors][pdfrest.PdfRestClient.convert_colors] + [convert_colors][pdfrest.PdfRestClient.convert_colors] applies preset or + uploaded ICC profiles and accepts a + [PdfRenderingIntent][pdfrest.types.PdfRenderingIntent] to control color + mapping ## Convert into or out of PDF diff --git a/examples/README.md b/examples/README.md index e35251d..59a5e8e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -34,6 +34,8 @@ supported interpreter matrix. - `examples/convert_structured_documents/convert_structured_documents_to_pdf_example.py` – convert Markdown, plain text, JSON, XML, and CSV documents to PDF with format-specific options. +- `examples/convert_colors/convert_pdf_colors_example.py` – convert an uploaded + PDF to sRGB with a perceptual rendering intent. - `examples/convert_to_markdown/convert_pdf_to_markdown_example.py` – convert a PDF to inline Markdown and a downloadable Markdown file. - `examples/convert_postscript_to_pdf/convert_postscript_with_job_options_example.py` diff --git a/examples/convert_colors/convert_pdf_colors_example.py b/examples/convert_colors/convert_pdf_colors_example.py new file mode 100644 index 0000000..c8cb518 --- /dev/null +++ b/examples/convert_colors/convert_pdf_colors_example.py @@ -0,0 +1,55 @@ +# /// script +# requires-python = ">=3.10" +# dependencies = ["pdfrest", "python-dotenv"] +# /// +"""Convert an uploaded PDF to sRGB with a perceptual rendering intent. + +This sample demonstrates how to: + +1. Load ``PDFREST_API_KEY`` from the environment or a local ``.env`` file. +2. Upload the checked-in ``examples/resources/report.pdf`` input. +3. Convert its colors to sRGB while preserving visual color relationships. +4. Print the generated PDF metadata. + +Set ``PDFREST_BASE_URL`` only for a self-hosted deployment. Run from the +repository root with +``uv run examples/convert_colors/convert_pdf_colors_example.py``. +""" + +from __future__ import annotations + +import os +from pathlib import Path + +from dotenv import load_dotenv + +from pdfrest import PdfRestClient +from pdfrest.types import PdfRenderingIntent + +RESOURCE_DIRECTORY = Path(__file__).resolve().parents[1] / "resources" +INPUT_PATH = RESOURCE_DIRECTORY / "report.pdf" + + +def convert_pdf_colors() -> None: + """Upload a PDF, convert its colors, and print the output metadata.""" + load_dotenv() + base_url = os.getenv("PDFREST_BASE_URL") + rendering_intent: PdfRenderingIntent = "perceptual" + + with PdfRestClient(base_url=base_url) as client: + uploaded_pdf = client.files.create_from_paths(INPUT_PATH)[0] + response = client.convert_colors( + uploaded_pdf, + color_profile="srgb", + rendering_intent=rendering_intent, + output="report-srgb", + ) + + print(f"Input ID: {response.input_id}") + print(f"Output file: {response.output_file.name}") + print(f"Output MIME type: {response.output_file.type}") + print(f"Output URL: {response.output_file.url}") + + +if __name__ == "__main__": # pragma: no cover - manual example + convert_pdf_colors()