34 lines
1.1 KiB
Python
34 lines
1.1 KiB
Python
"""Strip characters illegal in XML 1.0 / OOXML text nodes.
|
|||
|
|
|
||
|
|
PDF extractors sometimes emit C0 controls (e.g. \\x02 soft-break markers)
|
||
|
|
that lxml (python-docx) and openpyxl reject on write. Also strip unpaired
|
||
|
|
UTF-16 surrogates which can appear in broken PDF ToUnicode maps.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import re
|
||
|
|
|
||
|
|
# XML 1.0 Char production excludes C0 controls except TAB/LF/CR.
|
||
|
|
# Also drop soft hyphen (U+00AD), BOM, non-chars, and lone surrogates.
|
||
|
|
_ILLEGAL_OOXML_RE = re.compile(
|
||
|
|
r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f-\x9f\u00ad\ufeff\ufffe\uffff"
|
||
|
|
r"\ud800-\udfff]"
|
||
|
|
)
|
||
|
|
|
||
|
|
|
||
|
|
def sanitize_ooxml_text(value: object | None) -> str:
|
||
|
|
"""Return text safe for OOXML / SpreadsheetML string cells."""
|
||
|
|
if value is None:
|
||
|
|
return ""
|
||
|
|
if not isinstance(value, str):
|
||
|
|
value = str(value)
|
||
|
|
if not value:
|
||
|
|
return ""
|
||
|
|
# Drop unpaired surrogates that PDF extractors may leave in strings
|
||
|
|
try:
|
||
|
|
value.encode("utf-8")
|
||
|
|
except UnicodeEncodeError:
|
||
|
|
value = value.encode("utf-8", errors="surrogatepass").decode("utf-8", errors="ignore")
|
||
|
|
return _ILLEGAL_OOXML_RE.sub("", value)
|