Skip to content

Commit 5edd8e3

Browse files
models: Add detailed schemas for extracted text structure
- Introduced new models for enhanced text extraction support: - `ExtractedTextDocument`: Represents structured JSON output from text extraction. - `ExtractedTextWord`, `ExtractedTextWordStyle`, `ExtractedTextWordColor`, and related classes: Support word-level extraction with font, color, coordinates, and style data. - `ExtractedTextFullText` and `ExtractedTextFullTextPages`: Enable handling of extracted text in both document-level and page-level formats. - Updated `__init__.py` to include the new models in `__all__`. - Improved property methods for `ExtractedTextFullText` to provide convenient access to text representations (`document_text`, `pages`). Assisted-by: Codex
1 parent 9ff3a86 commit 5edd8e3

2 files changed

Lines changed: 341 additions & 0 deletions

File tree

src/pdfrest/models/__init__.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,15 @@
11
from .public import (
2+
ExtractedTextDocument,
3+
ExtractedTextFullText,
4+
ExtractedTextFullTextPage,
5+
ExtractedTextFullTextPages,
6+
ExtractedTextPoint,
7+
ExtractedTextWord,
8+
ExtractedTextWordColor,
9+
ExtractedTextWordCoordinates,
10+
ExtractedTextWordFont,
11+
ExtractedTextWordStyle,
12+
ExtractTextResponse,
213
PdfRestDeletionResponse,
314
PdfRestErrorResponse,
415
PdfRestFile,
@@ -12,6 +23,17 @@
1223
)
1324

1425
__all__ = [
26+
"ExtractTextResponse",
27+
"ExtractedTextDocument",
28+
"ExtractedTextFullText",
29+
"ExtractedTextFullTextPage",
30+
"ExtractedTextFullTextPages",
31+
"ExtractedTextPoint",
32+
"ExtractedTextWord",
33+
"ExtractedTextWordColor",
34+
"ExtractedTextWordCoordinates",
35+
"ExtractedTextWordFont",
36+
"ExtractedTextWordStyle",
1537
"PdfRestDeletionResponse",
1638
"PdfRestErrorResponse",
1739
"PdfRestFile",

src/pdfrest/models/public.py

Lines changed: 319 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,24 @@
1414
ConfigDict,
1515
Field,
1616
HttpUrl,
17+
RootModel,
1718
)
1819
from pydantic.json_schema import JsonSchemaValue
1920
from pydantic_core import CoreSchema
2021
from typing_extensions import override
2122

2223
__all__ = (
24+
"ExtractTextResponse",
25+
"ExtractedTextDocument",
26+
"ExtractedTextFullText",
27+
"ExtractedTextFullTextPage",
28+
"ExtractedTextFullTextPages",
29+
"ExtractedTextPoint",
30+
"ExtractedTextWord",
31+
"ExtractedTextWordColor",
32+
"ExtractedTextWordCoordinates",
33+
"ExtractedTextWordFont",
34+
"ExtractedTextWordStyle",
2335
"PdfRestDeletionResponse",
2436
"PdfRestErrorResponse",
2537
"PdfRestFile",
@@ -402,6 +414,313 @@ class TranslatePdfTextFileResponse(PdfRestFileBasedResponse):
402414
] = None
403415

404416

417+
class ExtractTextResponse(BaseModel):
418+
"""Response returned by the extracted-text tool."""
419+
420+
model_config = ConfigDict(extra="allow")
421+
422+
full_text: Annotated[
423+
str | None,
424+
Field(
425+
alias="fullText",
426+
validation_alias=AliasChoices("full_text", "fullText"),
427+
description="Inline extracted text when output_type is json.",
428+
default=None,
429+
),
430+
] = None
431+
input_id: Annotated[
432+
PdfRestFileID,
433+
Field(
434+
validation_alias=AliasChoices("input_id", "inputId"),
435+
description="The id of the input file.",
436+
),
437+
]
438+
warning: Annotated[
439+
str | None,
440+
Field(description="A warning that was generated during text extraction."),
441+
] = None
442+
443+
444+
class ExtractedTextPoint(BaseModel):
445+
"""A point in PDF coordinate space expressed in points."""
446+
447+
model_config = ConfigDict(extra="allow")
448+
449+
x: Annotated[
450+
float,
451+
Field(description="Horizontal position in PDF points."),
452+
]
453+
y: Annotated[
454+
float,
455+
Field(description="Vertical position in PDF points."),
456+
]
457+
458+
459+
class ExtractedTextWordCoordinates(BaseModel):
460+
"""Bounding box describing where a word appears on the page."""
461+
462+
model_config = ConfigDict(extra="allow")
463+
464+
top_left: Annotated[
465+
ExtractedTextPoint,
466+
Field(
467+
alias="topLeft",
468+
validation_alias=AliasChoices("top_left", "topLeft"),
469+
description="Upper-left corner of the word bounds.",
470+
),
471+
]
472+
top_right: Annotated[
473+
ExtractedTextPoint,
474+
Field(
475+
alias="topRight",
476+
validation_alias=AliasChoices("top_right", "topRight"),
477+
description="Upper-right corner of the word bounds.",
478+
),
479+
]
480+
bottom_left: Annotated[
481+
ExtractedTextPoint,
482+
Field(
483+
alias="bottomLeft",
484+
validation_alias=AliasChoices("bottom_left", "bottomLeft"),
485+
description="Lower-left corner of the word bounds.",
486+
),
487+
]
488+
bottom_right: Annotated[
489+
ExtractedTextPoint,
490+
Field(
491+
alias="bottomRight",
492+
validation_alias=AliasChoices("bottom_right", "bottomRight"),
493+
description="Lower-right corner of the word bounds.",
494+
),
495+
]
496+
497+
498+
class ExtractedTextWordColor(BaseModel):
499+
"""Font color applied to an extracted word."""
500+
501+
model_config = ConfigDict(extra="allow")
502+
503+
space: Annotated[
504+
str,
505+
Field(description="Color space name reported by pdfRest (e.g., DeviceRGB)."),
506+
]
507+
values: Annotated[
508+
list[float],
509+
Field(
510+
description="Numeric components in the reported color space.",
511+
min_length=1,
512+
),
513+
]
514+
515+
516+
class ExtractedTextWordFont(BaseModel):
517+
"""Font metadata applied to an extracted word."""
518+
519+
model_config = ConfigDict(extra="allow")
520+
521+
name: Annotated[
522+
str | None,
523+
Field(
524+
description="Reported font face name.",
525+
default=None,
526+
),
527+
] = None
528+
size: Annotated[
529+
float | None,
530+
Field(
531+
description="Font size in points.",
532+
default=None,
533+
),
534+
] = None
535+
536+
537+
class ExtractedTextWordStyle(BaseModel):
538+
"""Style information for an extracted word."""
539+
540+
model_config = ConfigDict(extra="allow")
541+
542+
color: Annotated[
543+
ExtractedTextWordColor,
544+
Field(description="Color information for the word."),
545+
]
546+
font: Annotated[
547+
ExtractedTextWordFont,
548+
Field(description="Font information for the word."),
549+
]
550+
551+
552+
class ExtractedTextWord(BaseModel):
553+
"""A single word extracted from a PDF page."""
554+
555+
model_config = ConfigDict(extra="allow")
556+
557+
text: Annotated[
558+
str,
559+
Field(description="Word content as rendered by the PDF."),
560+
]
561+
page: Annotated[
562+
int,
563+
Field(description="1-indexed page number containing the word.", ge=1),
564+
]
565+
coordinates: Annotated[
566+
ExtractedTextWordCoordinates | None,
567+
Field(
568+
description="Bounding box for the word when positional data is requested.",
569+
default=None,
570+
),
571+
] = None
572+
style: Annotated[
573+
ExtractedTextWordStyle | None,
574+
Field(
575+
description="Font/color details captured for the word.",
576+
default=None,
577+
),
578+
] = None
579+
580+
581+
class ExtractedTextFullTextPage(BaseModel):
582+
"""Per-page representation of the aggregated text content."""
583+
584+
model_config = ConfigDict(extra="allow")
585+
586+
page: Annotated[
587+
int,
588+
Field(description="1-indexed page number.", ge=1),
589+
]
590+
text: Annotated[
591+
str,
592+
Field(description="Concatenated text for the page."),
593+
]
594+
595+
596+
class ExtractedTextFullTextPages(BaseModel):
597+
"""Container for per-page text output."""
598+
599+
model_config = ConfigDict(extra="allow")
600+
601+
pages: Annotated[
602+
list[ExtractedTextFullTextPage],
603+
Field(
604+
description="Ordered text for each page present in the document.",
605+
min_length=1,
606+
),
607+
]
608+
609+
610+
class ExtractedTextFullText(RootModel[str | ExtractedTextFullTextPages]):
611+
"""
612+
Represents full-text extraction in either "document" (str) or "page" (object)
613+
modes while providing convenience accessors for both forms.
614+
"""
615+
616+
root: str | ExtractedTextFullTextPages
617+
618+
@property
619+
def document_text(self) -> str | None:
620+
"""
621+
Return the document-level string. Falls back to space-joining per-page text
622+
when only the page-structured payload is available.
623+
"""
624+
if isinstance(self.root, str):
625+
return self.root
626+
return " ".join(page.text for page in self.root.pages)
627+
628+
@property
629+
def pages(self) -> list[ExtractedTextFullTextPage]:
630+
"""
631+
Return page entries when pdfRest emits per-page text.
632+
Raises ValueError when the payload is in document-string mode.
633+
"""
634+
if isinstance(self.root, ExtractedTextFullTextPages):
635+
return self.root.pages
636+
msg = "full text payload was emitted in document mode; page data unavailable"
637+
raise ValueError(msg)
638+
639+
def iter_pages(self) -> list[ExtractedTextFullTextPage]:
640+
"""
641+
Convenience helper that provides a stable iterable without requiring
642+
callers to guard against the document-only representation.
643+
"""
644+
try:
645+
return self.pages
646+
except ValueError:
647+
return []
648+
649+
650+
class ExtractedTextDocument(BaseModel):
651+
"""Structured representation of the JSON output returned by extract_text_to_file."""
652+
653+
model_config = ConfigDict(extra="allow")
654+
655+
input_id: Annotated[
656+
PdfRestFileID,
657+
Field(
658+
alias="inputId",
659+
validation_alias=AliasChoices("input_id", "inputId"),
660+
description="Identifier of the uploaded PDF.",
661+
),
662+
]
663+
words: Annotated[
664+
list[ExtractedTextWord] | None,
665+
Field(
666+
description="Individual word records when word-level extraction is enabled.",
667+
default=None,
668+
),
669+
] = None
670+
full_text: Annotated[
671+
ExtractedTextFullText | None,
672+
Field(
673+
alias="fullText",
674+
validation_alias=AliasChoices("full_text", "fullText"),
675+
description="Full text output (document string or per-page content).",
676+
default=None,
677+
),
678+
] = None
679+
680+
681+
class ConvertToMarkdownResponse(BaseModel):
682+
"""Response returned by the markdown conversion tool."""
683+
684+
model_config = ConfigDict(extra="allow")
685+
686+
markdown: Annotated[
687+
str | None,
688+
Field(
689+
description="Inline markdown content when output_type is json.",
690+
default=None,
691+
),
692+
] = None
693+
input_id: Annotated[
694+
PdfRestFileID,
695+
Field(
696+
validation_alias=AliasChoices("input_id", "inputId"),
697+
description="The id of the input file.",
698+
),
699+
]
700+
output_url: Annotated[
701+
HttpUrl | None,
702+
Field(
703+
alias="outputUrl",
704+
validation_alias=AliasChoices("output_url", "outputUrl"),
705+
description="Download URL for file output.",
706+
default=None,
707+
),
708+
] = None
709+
output_id: Annotated[
710+
PdfRestFileID | None,
711+
Field(
712+
alias="outputId",
713+
validation_alias=AliasChoices("output_id", "outputId"),
714+
description="The id of the generated output when output_type is file.",
715+
default=None,
716+
),
717+
] = None
718+
warning: Annotated[
719+
str | None,
720+
Field(description="A warning that was generated during markdown conversion."),
721+
] = None
722+
723+
405724
class PdfRestInfoResponse(BaseModel):
406725
"""A response containing the output from the /info route."""
407726

0 commit comments

Comments
 (0)