@@ -38,7 +38,7 @@ def make_text_object(**overrides: object) -> dict[str, object]:
3838def _serialize_text_object_for_request (
3939 text_object : dict [str , object ],
4040) -> dict [str , object ]:
41- serialized = dict ( text_object )
41+ serialized = { key : value for key , value in text_object . items () if value is not None }
4242 # PdfAddTextObjectModel defines these fields as floats, so Pydantic serializes
4343 # integer inputs as 200.0/45.0/etc. Mirror that to keep wire assertions exact.
4444 for key in ("max_width" , "rotation" , "text_size" , "x" , "y" ):
@@ -226,6 +226,275 @@ def handler(_: httpx.Request) -> httpx.Response:
226226 )
227227
228228
229+ def test_add_text_to_pdf_rejects_non_integer_rgb_values (
230+ monkeypatch : pytest .MonkeyPatch ,
231+ ) -> None :
232+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
233+
234+ def handler (_ : httpx .Request ) -> httpx .Response :
235+ pytest .fail ("Request should not be sent when validation fails." )
236+
237+ transport = httpx .MockTransport (handler )
238+ with (
239+ PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client ,
240+ pytest .raises (
241+ ValidationError ,
242+ match = re .escape ("text_color_rgb values must be integers." ),
243+ ),
244+ ):
245+ client .add_text_to_pdf (
246+ make_pdf_file (PdfRestFileID .generate (1 )),
247+ text_objects = [make_text_object (text_color_rgb = (0 , "invalid" , 2 ))],
248+ )
249+
250+
251+ def test_add_text_to_pdf_rejects_non_integer_cmyk_values (
252+ monkeypatch : pytest .MonkeyPatch ,
253+ ) -> None :
254+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
255+
256+ def handler (_ : httpx .Request ) -> httpx .Response :
257+ pytest .fail ("Request should not be sent when validation fails." )
258+
259+ transport = httpx .MockTransport (handler )
260+ with (
261+ PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client ,
262+ pytest .raises (
263+ ValidationError ,
264+ match = re .escape ("text_color_cmyk values must be integers." ),
265+ ),
266+ ):
267+ client .add_text_to_pdf (
268+ make_pdf_file (PdfRestFileID .generate (1 )),
269+ text_objects = [
270+ make_text_object (text_color_rgb = None , text_color_cmyk = (0 , 0 , "bad" , 0 ))
271+ ],
272+ )
273+
274+
275+ def test_add_text_to_pdf_rejects_incomplete_cmyk_values (
276+ monkeypatch : pytest .MonkeyPatch ,
277+ ) -> None :
278+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
279+
280+ def handler (_ : httpx .Request ) -> httpx .Response :
281+ pytest .fail ("Request should not be sent when validation fails." )
282+
283+ transport = httpx .MockTransport (handler )
284+ with (
285+ PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client ,
286+ pytest .raises (
287+ ValidationError ,
288+ match = re .escape ("text_color_cmyk must have exactly 4 values." ),
289+ ),
290+ ):
291+ client .add_text_to_pdf (
292+ make_pdf_file (PdfRestFileID .generate (1 )),
293+ text_objects = [
294+ make_text_object (text_color_rgb = None , text_color_cmyk = (0 , 0 , 0 ))
295+ ],
296+ )
297+
298+
299+ def test_add_text_to_pdf_rejects_non_sequence_rgb_color (
300+ monkeypatch : pytest .MonkeyPatch ,
301+ ) -> None :
302+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
303+
304+ def handler (_ : httpx .Request ) -> httpx .Response :
305+ pytest .fail ("Request should not be sent when validation fails." )
306+
307+ transport = httpx .MockTransport (handler )
308+ with (
309+ PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client ,
310+ pytest .raises (
311+ ValidationError ,
312+ match = re .escape ("Must be a list, or a comma separated string." ),
313+ ),
314+ ):
315+ client .add_text_to_pdf (
316+ make_pdf_file (PdfRestFileID .generate (1 )),
317+ text_objects = [make_text_object (text_color_rgb = 123 )],
318+ )
319+
320+
321+ def test_add_text_to_pdf_rejects_incomplete_rgb_values (
322+ monkeypatch : pytest .MonkeyPatch ,
323+ ) -> None :
324+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
325+
326+ def handler (_ : httpx .Request ) -> httpx .Response :
327+ pytest .fail ("Request should not be sent when validation fails." )
328+
329+ transport = httpx .MockTransport (handler )
330+ with (
331+ PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client ,
332+ pytest .raises (
333+ ValidationError ,
334+ match = re .escape ("text_color_rgb must have exactly 3 values." ),
335+ ),
336+ ):
337+ client .add_text_to_pdf (
338+ make_pdf_file (PdfRestFileID .generate (1 )),
339+ text_objects = [make_text_object (text_color_rgb = (1 , 2 ))],
340+ )
341+
342+
343+ def test_add_text_to_pdf_rejects_multiple_color_modes (
344+ monkeypatch : pytest .MonkeyPatch ,
345+ ) -> None :
346+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
347+
348+ def handler (_ : httpx .Request ) -> httpx .Response :
349+ pytest .fail ("Request should not be sent when validation fails." )
350+
351+ transport = httpx .MockTransport (handler )
352+ with (
353+ PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client ,
354+ pytest .raises (
355+ ValidationError ,
356+ match = re .escape ("Provide only one of text_color_rgb or text_color_cmyk." ),
357+ ),
358+ ):
359+ client .add_text_to_pdf (
360+ make_pdf_file (PdfRestFileID .generate (1 )),
361+ text_objects = [make_text_object (text_color_cmyk = (0 , 0 , 0 , 0 ))],
362+ )
363+
364+
365+ def test_add_text_to_pdf_rejects_invalid_page_selector (
366+ monkeypatch : pytest .MonkeyPatch ,
367+ ) -> None :
368+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
369+
370+ def handler (_ : httpx .Request ) -> httpx .Response :
371+ pytest .fail ("Request should not be sent when validation fails." )
372+
373+ transport = httpx .MockTransport (handler )
374+ with (
375+ PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client ,
376+ pytest .raises (
377+ ValidationError ,
378+ match = re .escape ('page must be a positive integer or "all".' ),
379+ ),
380+ ):
381+ client .add_text_to_pdf (
382+ make_pdf_file (PdfRestFileID .generate (1 )),
383+ text_objects = [make_text_object (page = "zero" )],
384+ )
385+
386+
387+ def test_add_text_to_pdf_rejects_disallowed_output_prefix (
388+ monkeypatch : pytest .MonkeyPatch ,
389+ ) -> None :
390+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
391+
392+ def handler (_ : httpx .Request ) -> httpx .Response :
393+ pytest .fail ("Request should not be sent when validation fails." )
394+
395+ transport = httpx .MockTransport (handler )
396+ with (
397+ PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client ,
398+ pytest .raises (
399+ ValidationError ,
400+ match = re .escape ("The output prefix must not contain `..`." ),
401+ ),
402+ ):
403+ client .add_text_to_pdf (
404+ make_pdf_file (PdfRestFileID .generate (1 )),
405+ text_objects = [make_text_object ()],
406+ output = "bad..prefix" ,
407+ )
408+
409+
410+ def test_add_text_to_pdf_accepts_comma_separated_rgb_string (
411+ monkeypatch : pytest .MonkeyPatch ,
412+ ) -> None :
413+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
414+ pdf_file = make_pdf_file (PdfRestFileID .generate (1 ))
415+ output_id = str (PdfRestFileID .generate ())
416+ text_object = make_text_object (text_color_rgb = "10,20,30" , text_color_cmyk = None )
417+
418+ def handler (request : httpx .Request ) -> httpx .Response :
419+ if request .method == "POST" and request .url .path == "/pdf-with-added-text" :
420+ payload = json .loads (request .content .decode ("utf-8" ))
421+ assert payload ["text_objects" ] == json .dumps (
422+ [_serialize_text_object_for_request (text_object )],
423+ separators = ("," , ":" ),
424+ )
425+ return httpx .Response (
426+ 200 ,
427+ json = {
428+ "inputId" : [pdf_file .id ],
429+ "outputId" : [output_id ],
430+ },
431+ )
432+ if request .method == "GET" and request .url .path == f"/resource/{ output_id } " :
433+ return httpx .Response (
434+ 200 ,
435+ json = build_file_info_payload (
436+ output_id ,
437+ "comma-rgb.pdf" ,
438+ "application/pdf" ,
439+ ),
440+ )
441+ msg = f"Unexpected request { request .method } { request .url } "
442+ raise AssertionError (msg )
443+
444+ transport = httpx .MockTransport (handler )
445+ with PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client :
446+ response = client .add_text_to_pdf (
447+ pdf_file ,
448+ text_objects = [text_object ],
449+ )
450+
451+ assert response .output_file .name == "comma-rgb.pdf"
452+
453+
454+ def test_add_text_to_pdf_accepts_numeric_page_string (
455+ monkeypatch : pytest .MonkeyPatch ,
456+ ) -> None :
457+ monkeypatch .delenv ("PDFREST_API_KEY" , raising = False )
458+ pdf_file = make_pdf_file (PdfRestFileID .generate (1 ))
459+ output_id = str (PdfRestFileID .generate ())
460+ text_object = make_text_object (page = "2" )
461+
462+ def handler (request : httpx .Request ) -> httpx .Response :
463+ if request .method == "POST" and request .url .path == "/pdf-with-added-text" :
464+ payload = json .loads (request .content .decode ("utf-8" ))
465+ assert payload ["text_objects" ] == json .dumps (
466+ [_serialize_text_object_for_request (make_text_object (page = 2 ))],
467+ separators = ("," , ":" ),
468+ )
469+ return httpx .Response (
470+ 200 ,
471+ json = {
472+ "inputId" : [pdf_file .id ],
473+ "outputId" : [output_id ],
474+ },
475+ )
476+ if request .method == "GET" and request .url .path == f"/resource/{ output_id } " :
477+ return httpx .Response (
478+ 200 ,
479+ json = build_file_info_payload (
480+ output_id ,
481+ "numeric-page.pdf" ,
482+ "application/pdf" ,
483+ ),
484+ )
485+ msg = f"Unexpected request { request .method } { request .url } "
486+ raise AssertionError (msg )
487+
488+ transport = httpx .MockTransport (handler )
489+ with PdfRestClient (api_key = VALID_API_KEY , transport = transport ) as client :
490+ response = client .add_text_to_pdf (
491+ pdf_file ,
492+ text_objects = [text_object ],
493+ )
494+
495+ assert response .output_file .name == "numeric-page.pdf"
496+
497+
229498def test_add_text_to_pdf_text_size_bounds (
230499 monkeypatch : pytest .MonkeyPatch ,
231500) -> None :
@@ -333,6 +602,7 @@ def handler(request: httpx.Request) -> httpx.Response:
333602 assert request .headers ["X-Test" ] == "async"
334603 payload = json .loads (request .content .decode ("utf-8" ))
335604 assert payload ["text_size" ] == 18
605+ assert payload ["output" ] == "async-custom-output"
336606 assert payload ["text_objects" ] == json .dumps (
337607 [_serialize_text_object_for_request (overridden_text_object )],
338608 separators = ("," , ":" ),
@@ -364,6 +634,7 @@ def handler(request: httpx.Request) -> httpx.Response:
364634 response = await client .add_text_to_pdf (
365635 pdf_file ,
366636 text_objects = [make_text_object (text_size = 18 )],
637+ output = "async-custom-output" ,
367638 extra_query = {"trace" : "true" },
368639 extra_headers = {"X-Test" : "async" },
369640 extra_body = {
0 commit comments