all2md.utils.attachments

Unified attachment handling utilities for all2md conversion modules.

This module provides common functions for handling attachments (images and files) across all conversion modules in the all2md library. It implements the unified AttachmentMode system with consistent behavior across different parsers.

The attachment handling modes are: - “skip”: Remove attachments completely - “alt_text”: Use alt-text for images, filename for files - “save”: Save to folder and reference with markdown links - “base64”: Embed as base64 data URIs (images only)

Functions

  • process_attachment: Main function for processing attachments based on mode

  • extract_pptx_image_data: Extract image data from PowerPoint shapes

  • extract_docx_image_data: Extract image data from Word document relationships

all2md.utils.attachments.sanitize_footnote_label(attachment_name: str) str

Sanitize attachment name for use as a Markdown footnote label.

Footnote labels in Markdown cannot contain spaces or many special characters without breaking rendering. This function creates a safe label by normalizing the attachment name.

Parameters:

attachment_name (str) – Original attachment filename

Returns:

Sanitized footnote label safe for Markdown

Return type:

str

Examples

>>> sanitize_footnote_label("my image.png")
'my_image'
>>> sanitize_footnote_label("file (1).jpg")
'file_1'
>>> sanitize_footnote_label("document%20name.pdf")
'document_20name'
all2md.utils.attachments.sanitize_attachment_filename(filename: str, max_length: int = 255, preserve_case: bool = False, allow_unicode: bool = False) str

Sanitize an attachment filename for secure file system storage.

This function normalizes Unicode characters, removes dangerous patterns, and ensures cross-platform compatibility while preventing security issues.

By default, this function is conservative and may be lossy: - Converts to lowercase (unless preserve_case=True) - Removes non-ASCII characters (unless allow_unicode=True) - Removes special characters except alphanumeric, underscore, dot, and hyphen

Parameters:
  • filename (str) – Original filename to sanitize

  • max_length (int, default 255) – Maximum length for the sanitized filename

  • preserve_case (bool, default False) – If True, preserve original case. If False, convert to lowercase for cross-platform compatibility (case-insensitive filesystems).

  • allow_unicode (bool, default False) – If True, allow Unicode letters and numbers (e.g., Chinese, Arabic). If False, only allow ASCII alphanumeric characters. Note: Unicode filenames may cause issues on some systems.

Returns:

Sanitized filename safe for file system use

Return type:

str

Raises:

ValueError – If the filename contains potentially malicious patterns that cannot be sanitized

Examples

>>> sanitize_attachment_filename("test́.png")  # test with combining accent
'test.png'
>>> sanitize_attachment_filename("../../../etc/passwd")
'passwd'
>>> sanitize_attachment_filename("file<>|name?.txt")
'filename.txt'
>>> sanitize_attachment_filename("Test.PNG", preserve_case=True)
'Test.PNG'
>>> sanitize_attachment_filename("文件.txt", allow_unicode=True)
'文件.txt'
all2md.utils.attachments.ensure_unique_attachment_path(base_path: Path, max_attempts: int = 1000, atomic: bool = True) Path

Ensure a unique file path by adding numeric suffixes for collisions.

Thread Safety and Race Conditions

When atomic=True (default), this function uses atomic file operations (os.open with O_CREAT | O_EXCL) to prevent TOCTOU race conditions. This makes it safe for: - Concurrent file creation from multiple processes - Concurrent file creation from multiple threads

When atomic=True, a 0-byte placeholder file is created at the returned path. The caller should overwrite this file with actual content.

When atomic=False, the function uses non-atomic existence checks, which is suitable for single-threaded usage only.

param base_path:

The desired base path for the attachment

type base_path:

Path

param max_attempts:

Maximum number of collision resolution attempts

type max_attempts:

int, default 1000

param atomic:

If True, use atomic file creation to prevent race conditions. If False, use simple existence checks (not thread-safe).

type atomic:

bool, default True

returns:

A unique file path. When atomic=True, a placeholder file exists at this path that should be overwritten.

rtype:

Path

raises RuntimeError:

If unable to find a unique path after max_attempts

raises OSError:

If atomic creation fails for reasons other than file collision (e.g., permission denied, parent directory doesn’t exist)

Examples

>>> # If image.png exists, returns image-1.png (and creates placeholder if atomic=True)
>>> ensure_unique_attachment_path(Path("./attachments/image.png"))
Path('./attachments/image-1.png')

Notes

When atomic=True, the caller MUST write to the returned path, as a 0-byte placeholder file has been created. If the caller fails to write, the placeholder should be cleaned up.

For typical single-process document conversion, atomic=False is adequate. Use atomic=True when concurrent access is expected.

all2md.utils.attachments.process_attachment(attachment_data: bytes | None, attachment_name: str, alt_text: str = '', attachment_mode: Literal['skip', 'alt_text', 'save', 'base64'] = 'alt_text', attachment_output_dir: str | None = None, attachment_base_url: str | None = None, is_image: bool = True, alt_text_mode: Literal['default', 'plain_filename', 'strict_markdown', 'footnote'] = 'default', allowed_output_base_dirs: list[str | Path] | None = None, block_sensitive_paths: bool = True) dict[str, Any]

Process an attachment according to the specified mode.

Parameters:
  • attachment_data (bytes | None) – Raw attachment data, or None if not available

  • attachment_name (str) – Name/filename of the attachment

  • alt_text (str, default "") – Alt text for images or description for files

  • attachment_mode (AttachmentMode, default "alt_text") – How to handle the attachment

  • attachment_output_dir (str | None, default None) – Directory to save attachments in save mode. For security, the directory is validated to ensure it stays within the current working directory and does not contain path traversal patterns. If validation fails, falls back to alt_text mode. Defaults to “attachments” if None.

  • attachment_base_url (str | None, default None) – Base URL for resolving relative URLs in save mode. Note: Only the filename (not the directory structure) is appended to this URL. For example, if attachment_output_dir=”attachments/images” and attachment_base_url=”https://example.com/assets/”, the resulting URL will be “https://example.com/assets/filename.png” (not “…/assets/attachments/images/filename.png”)

  • is_image (bool, default True) – Whether this is an image attachment

  • alt_text_mode (AltTextMode, default "default") – How to render alt-text content: - “default”: Current behavior - ![alt] for images, [filename] for files - “plain_filename”: Render non-images as plain filename text - “strict_markdown”: Use ![alt](#) format for proper Markdown structure - “footnote”: Use footnote references for accessibility

  • allowed_output_base_dirs (list[str | Path] | None, default None) – Optional allowlist of base directories for output validation. If provided, attachment_output_dir must be within one of these directories. When None, uses default security checks (blocks path traversal and sensitive paths). Useful for server environments where output should be restricted to specific directories.

  • block_sensitive_paths (bool, default True) – Block output to sensitive system directories (/etc, /sys, C:\Windows, etc.). Set to False to allow writing to any directory (use with caution). Only applies when allowed_output_base_dirs is None.

Returns:

Dictionary with keys: - “markdown”: str - Markdown representation of the attachment - “footnote_label”: str | None - Footnote label if alt_text_mode is “footnote” - “footnote_content”: str | None - Content for footnote definition - “url”: str - URL/path for the attachment (empty for alt_text mode) - “source_data”: str | None - Source of the attachment data (e.g., “base64”, “downloaded”)

Return type:

dict[str, Any]

all2md.utils.attachments.extract_pptx_image_data(shape: Any) bytes | None

Extract raw image data from a PowerPoint shape.

Parameters:

shape (Any) – PowerPoint shape object with image property

Returns:

Raw image bytes, or None if extraction fails

Return type:

bytes | None

all2md.utils.attachments.extract_docx_image_data(parent: Any, blip_rId: str) tuple[bytes | None, str | None]

Extract image data and format information from Word document relationships.

Parameters:
  • parent (Any) – Word document parent element

  • blip_rId (str) – Relationship ID for the image

Returns:

Tuple of (raw image bytes, file extension), or (None, None) if extraction fails

Return type:

tuple[bytes | None, str | None]

all2md.utils.attachments.generate_attachment_filename(base_stem: str, attachment_type: str = 'img', format_type: str = 'general', page_num: int | None = None, slide_num: int | None = None, sequence_num: int = 1, extension: str = 'png') str

Generate standardized attachment filenames across all parsers.

Parameters:
  • base_stem (str) – Base filename stem (without extension) from the source document

  • attachment_type (str, default "img") – Type of attachment (e.g., “img”, “file”)

  • format_type (str, default "general") – Format context - one of: - “pdf”: For PDF pages - generates {stem}_p{page}_img{n}.{ext} - “pptx”: For PowerPoint slides - generates {stem}_slide{n}_img{m}.{ext} - “general”: For other formats - generates {stem}_img{n}.{ext}

  • page_num (int | None, default None) – Page number (1-based) for PDF format

  • slide_num (int | None, default None) – Slide number (1-based) for PPTX format

  • sequence_num (int, default 1) – Sequence number for multiple attachments

  • extension (str, default "png") – File extension without dot

Returns:

Standardized filename

Return type:

str

Examples

>>> generate_attachment_filename("document", format_type="pdf", page_num=1, sequence_num=2)
'document_p1_img2.png'
>>> generate_attachment_filename("presentation", format_type="pptx", slide_num=3, sequence_num=1)
'presentation_slide3_img1.png'
>>> generate_attachment_filename("article", format_type="general", sequence_num=5)
'article_img5.png'
class all2md.utils.attachments.AttachmentSequencer

Bases: Protocol

Protocol for attachment filename sequencer callables.

This protocol defines the signature for functions returned by create_attachment_sequencer(). The sequencer generates unique, sequential filenames for attachments based on format-specific rules.

Thread Safety

Sequencers created by create_attachment_sequencer() are thread-safe. They use internal locking to protect shared mutable state, allowing safe concurrent use from multiple threads.

param base_stem:

Base filename stem (without extension) from the source document

type base_stem:

str

param format_type:

Format context - one of “pdf”, “pptx”, or “general”

type format_type:

str, default “general”

param **kwargs:

Additional keyword arguments: - page_num : int | None - Page number for PDF format (required if format_type=”pdf”) - slide_num : int | None - Slide number for PPTX format (required if format_type=”pptx”) - extension : str - File extension without dot (default: “png”) - attachment_type : str - Type of attachment (default: “img”)

type **kwargs:

Any

returns:

Tuple of (generated filename, sequence number)

rtype:

tuple[str, int]

__init__(*args, **kwargs)
all2md.utils.attachments.create_attachment_sequencer() AttachmentSequencer

Create a closure that tracks attachment sequence numbers to prevent duplicates.

Thread Safety

The returned sequencer is thread-safe. It uses a lock to protect the internal mutable state (used_filenames set and sequence_counters dict).

This allows safe concurrent use from multiple threads, though each document conversion typically uses its own sequencer instance.

returns:

Function that generates sequential attachment filenames and tracks usage

rtype:

callable

Examples

>>> sequencer = create_attachment_sequencer()
>>> sequencer("doc", "pdf", page_num=1)  # Returns: ('doc_p1_img1.png', 1)
>>> sequencer("doc", "pdf", page_num=1)  # Returns: ('doc_p1_img2.png', 2)
>>> sequencer("doc", "pdf", page_num=2)  # Returns: ('doc_p2_img1.png', 1)