Skip to content
17 changes: 17 additions & 0 deletions docling_core/transforms/serializer/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from typing_extensions import deprecated

from docling_core.types.doc.document import (
AttachmentItem,
DocItem,
DoclingDocument,
FloatingItem,
Expand Down Expand Up @@ -204,6 +205,22 @@ def serialize(
...


class BaseAttachmentSerializer(ABC):
"""Base class for attachment item serializers."""

@abstractmethod
def serialize(
self,
*,
item: AttachmentItem,
doc_serializer: "BaseDocSerializer",
doc: DoclingDocument,
**kwargs: Any,
) -> SerializationResult:
"""Serializes the passed attachment item."""
...


class BaseDocSerializer(ABC):
"""Base class for document serializers."""

Expand Down
23 changes: 23 additions & 0 deletions docling_core/transforms/serializer/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@

from docling_core.transforms.serializer.base import (
BaseAnnotationSerializer,
BaseAttachmentSerializer,
BaseDocSerializer,
BaseFallbackSerializer,
BaseFormSerializer,
Expand All @@ -36,6 +37,7 @@
Span,
)
from docling_core.types.doc import (
AttachmentItem,
ContentLayer,
DescriptionAnnotation,
DocItem,
Expand Down Expand Up @@ -247,6 +249,7 @@ class DocSerializer(BaseModel, BaseDocSerializer):

meta_serializer: Optional[BaseMetaSerializer] = None
annotation_serializer: BaseAnnotationSerializer
attachment_serializer: Optional[BaseAttachmentSerializer] = None

params: CommonParams = CommonParams()

Expand Down Expand Up @@ -477,6 +480,26 @@ def serialize(
doc=self.doc,
**my_kwargs,
)
elif isinstance(my_item, AttachmentItem):
if self.attachment_serializer is not None:
part = (
self.attachment_serializer.serialize(
item=my_item,
doc_serializer=self,
doc=self.doc,
**my_kwargs,
)
if my_item.self_ref not in self.get_excluded_refs(**kwargs)
else empty_res
)
else:
part = self.fallback_serializer.serialize(
item=my_item,
doc_serializer=self,
doc=self.doc,
visited=my_visited,
**my_kwargs,
)
elif isinstance(my_item, _PageBreakNode):
part = _PageBreakSerResult(
text=self._create_page_break(node=my_item),
Expand Down
42 changes: 42 additions & 0 deletions docling_core/transforms/serializer/doctags.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from docling_core.transforms.serializer.base import (
BaseAnnotationSerializer,
BaseAttachmentSerializer,
BaseDocSerializer,
BaseFallbackSerializer,
BaseFormSerializer,
Expand All @@ -27,6 +28,7 @@
create_ser_result,
)
from docling_core.types.doc.document import (
AttachmentItem,
BoundingBox,
CodeItem,
DocItem,
Expand Down Expand Up @@ -567,6 +569,45 @@ def serialize(self, *, item: DocItem, **kwargs: Any) -> SerializationResult:
return create_ser_result()


class DocTagsAttachmentSerializer(BaseModel, BaseAttachmentSerializer):
"""DocTags-specific attachment serializer."""

@override
def serialize(
self,
*,
item: AttachmentItem,
doc_serializer: BaseDocSerializer,
doc: DoclingDocument,
**kwargs: Any,
) -> SerializationResult:
"""Serializes the passed attachment item."""
if isinstance(doc_serializer, DocTagsDocSerializer):
params = doc_serializer.params.merge_with_patch(kwargs)
else:
params = DocTagsParams(**kwargs)
parts: list[str] = []
if params.add_location:
location = item.get_location_tokens(
doc=doc,
xsize=params.xsize,
ysize=params.ysize,
self_closing=params.do_self_closing,
)
if location:
parts.append(location)
if params.add_content:
if item.status == "converted" and item.target:
parts.append(f"{item.name} ({item.target})")
else:
reason = item.status.replace("_", " ")
parts.append(f"{item.name} (not converted: {reason})")
text = "".join(parts)
if text:
text = _wrap(text=text, wrap_tag=DocumentToken.ATTACHMENT.value)
return create_ser_result(text=text, span_source=item)


class DocTagsDocSerializer(DocSerializer):
"""DocTags-specific document serializer."""

Expand All @@ -581,6 +622,7 @@ class DocTagsDocSerializer(DocSerializer):
inline_serializer: BaseInlineSerializer = DocTagsInlineSerializer()

annotation_serializer: BaseAnnotationSerializer = DocTagsAnnotationSerializer()
attachment_serializer: BaseAttachmentSerializer = DocTagsAttachmentSerializer()

params: DocTagsParams = DocTagsParams()

Expand Down
26 changes: 26 additions & 0 deletions docling_core/transforms/serializer/html.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

from docling_core.transforms.serializer.base import (
BaseAnnotationSerializer,
BaseAttachmentSerializer,
BaseDocSerializer,
BaseFallbackSerializer,
BaseFormSerializer,
Expand All @@ -45,6 +46,7 @@
from docling_core.transforms.visualizer.base import BaseVisualizer
from docling_core.types.doc.base import ImageRefMode
from docling_core.types.doc.document import (
AttachmentItem,
BaseMeta,
CodeItem,
CodeMetaField,
Expand Down Expand Up @@ -901,6 +903,29 @@ def serialize(
return create_ser_result(text=inline_html, span_source=parts)


class HTMLAttachmentSerializer(BaseModel, BaseAttachmentSerializer):
"""HTML-specific attachment serializer."""

@override
def serialize(
self,
*,
item: AttachmentItem,
doc_serializer: "BaseDocSerializer",
doc: DoclingDocument,
**kwargs: Any,
) -> SerializationResult:
"""Serializes the passed attachment item."""
if item.self_ref in doc_serializer.get_excluded_refs(**kwargs):
return create_ser_result()
if item.status == "converted" and item.target:
text = f'<a href="{html.escape(str(item.target))}">{html.escape(item.name)}</a>'
else:
reason = item.status.replace("_", " ")
text = f"{html.escape(item.name)} (not converted: {html.escape(reason)})"
return create_ser_result(text=f"<p>{text}</p>", span_source=item)


class HTMLFallbackSerializer(BaseFallbackSerializer):
"""HTML-specific fallback serializer."""

Expand Down Expand Up @@ -1076,6 +1101,7 @@ class HTMLDocSerializer(DocSerializer):

meta_serializer: BaseMetaSerializer = HTMLMetaSerializer()
annotation_serializer: BaseAnnotationSerializer = HTMLAnnotationSerializer()
attachment_serializer: BaseAttachmentSerializer = HTMLAttachmentSerializer()

params: HTMLParams = HTMLParams()

Expand Down
45 changes: 45 additions & 0 deletions docling_core/transforms/serializer/markdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

from docling_core.transforms.serializer.base import (
BaseAnnotationSerializer,
BaseAttachmentSerializer,
BaseDocSerializer,
BaseFallbackSerializer,
BaseFormSerializer,
Expand All @@ -34,6 +35,7 @@
create_ser_result,
)
from docling_core.types.doc import (
AttachmentItem,
BaseMeta,
CodeItem,
ContentLayer,
Expand Down Expand Up @@ -842,6 +844,35 @@ def serialize(
)


def _format_attachment_link(item: AttachmentItem) -> str:
"""Return the markdown representation of an attachment item."""
if item.status == "converted" and item.target:
return f"[{item.name}]({item.target})"
reason = item.status.replace("_", " ")
return f"{item.name} (not converted: {reason})"


class MarkdownAttachmentSerializer(BaseModel, BaseAttachmentSerializer):
"""Markdown-specific attachment serializer."""

@override
def serialize(
self,
*,
item: AttachmentItem,
doc_serializer: "BaseDocSerializer",
doc: DoclingDocument,
**kwargs: Any,
) -> SerializationResult:
"""Serializes the passed attachment item."""
if item.prov and item.self_ref not in doc_serializer.get_excluded_refs(**kwargs):
return create_ser_result(
text=_format_attachment_link(item),
span_source=item,
)
return create_ser_result()


class MarkdownDocSerializer(DocSerializer):
"""Markdown-specific document serializer."""

Expand All @@ -857,6 +888,7 @@ class MarkdownDocSerializer(DocSerializer):

meta_serializer: BaseMetaSerializer = MarkdownMetaSerializer()
annotation_serializer: BaseAnnotationSerializer = MarkdownAnnotationSerializer()
attachment_serializer: BaseAttachmentSerializer = MarkdownAttachmentSerializer()

params: MarkdownParams = MarkdownParams()

Expand Down Expand Up @@ -948,6 +980,19 @@ def serialize_doc(
for full_match, _, _ in self._get_page_breaks(text=text_res):
text_res = text_res.replace(full_match, page_sep)

params = self.params.merge_with_patch(patch=kwargs)
if DocItemLabel.ATTACHMENT in params.labels:
excluded_refs = self.get_excluded_refs(**kwargs)
unpositioned = [att for att in self.doc.attachments if not att.prov and att.self_ref not in excluded_refs]
if unpositioned:
section_parts = ["## Attachments"]
section_parts.extend(_format_attachment_link(att) for att in unpositioned)
section_text = "\n\n".join(section_parts)
if text_res:
text_res = f"{text_res}\n\n{section_text}"
else:
text_res = section_text

return create_ser_result(text=text_res, span_source=parts)

@override
Expand Down
1 change: 1 addition & 0 deletions docling_core/types/doc/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
DocTagsPage,
)
from docling_core.types.doc.document import DoclingDocument
from docling_core.types.doc.items.attachment import AttachmentItem, AttachmentStatus
from docling_core.types.doc.items.code import CodeItem
from docling_core.types.doc.items.content import ContentItem
from docling_core.types.doc.items.form import (
Expand Down
1 change: 1 addition & 0 deletions docling_core/types/doc/common/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
DocItemLabel.FIELD_HINT,
DocItemLabel.MARKER,
DocItemLabel.HANDWRITTEN_TEXT,
DocItemLabel.ATTACHMENT,
}


Expand Down
Loading
Loading