Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion docs/api-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
55 changes: 55 additions & 0 deletions examples/convert_colors/convert_pdf_colors_example.py
Original file line number Diff line number Diff line change
@@ -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()
9 changes: 9 additions & 0 deletions src/pdfrest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@
PdfPageSize,
PdfPresetColorProfile,
PdfRedactionInstruction,
PdfRenderingIntent,
PdfRestriction,
PdfRGBColor,
PdfSignatureConfiguration,
Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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.
Expand All @@ -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

Expand Down
5 changes: 5 additions & 0 deletions src/pdfrest/models/_internal.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
PdfPageOrientation,
PdfPageSize,
PdfPresetColorProfile,
PdfRenderingIntent,
PdfRestriction,
PdfStructuredTextDataPresentation,
PdfStructuredTextFontName,
Expand Down Expand Up @@ -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),
Expand Down
2 changes: 2 additions & 0 deletions src/pdfrest/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
PdfRedactionInstruction,
PdfRedactionPreset,
PdfRedactionType,
PdfRenderingIntent,
PdfRestriction,
PdfRGBColor,
PdfSignatureConfiguration,
Expand Down Expand Up @@ -135,6 +136,7 @@
"PdfRedactionInstruction",
"PdfRedactionPreset",
"PdfRedactionType",
"PdfRenderingIntent",
"PdfRestriction",
"PdfSignatureConfiguration",
"PdfSignatureCredentials",
Expand Down
22 changes: 22 additions & 0 deletions src/pdfrest/types/public.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"PdfRedactionInstruction",
"PdfRedactionPreset",
"PdfRedactionType",
"PdfRenderingIntent",
"PdfRestriction",
"PdfSignatureConfiguration",
"PdfSignatureCredentials",
Expand Down Expand Up @@ -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"]
110 changes: 109 additions & 1 deletion tests/live/test_live_convert_colors.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,18 @@

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

ALL_COLOR_PROFILES: tuple[PdfPresetColorProfile, ...] = cast(
tuple[PdfPresetColorProfile, ...],
get_args(PdfPresetColorProfile),
)
ALL_RENDERING_INTENTS: tuple[PdfRenderingIntent, ...] = cast(
tuple[PdfRenderingIntent, ...],
get_args(PdfRenderingIntent),
)


@pytest.fixture(scope="module")
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading