Skip to content

[Bug] Blend2D font resolver throws "Cannot convert character sequence" on Windows when LOCALAPPDATA contains non-ASCII characters (breaks default docling PDF backend) #354

Description

@wittjeff

Bug

On a Windows machine whose user profile path contains non-ASCII characters (Korean in the downstream report, but any character outside ASCII in a non-UTF-8 ANSI code page will do), constructing _threaded_pdf_renderer throws:

RuntimeError: filesystem error: Cannot convert character sequence: Illegal byte sequence

Since docling 2.1xx defaults PdfFormatOption.backend to ThreadedDoclingParseDocumentBackend, this breaks every PDF conversion for such users, regardless of where the PDF itself lives. The non-threaded render_image path hits the same code lazily on the first page render (blend2d_font_resolver::default_resolver()), so it is not specific to the threaded API.

Downstream report with full reproduction: opendataloader-project/opendataloader-pdf#725. The reporter worked around it by switching docling to DoclingParseDocumentBackend, which happens to render page images with pypdfium2 and so never constructs the font resolver.

Cause

src/render/blend2d_font_resolver.h:

inline std::optional<std::string> blend2d_font_resolver::getenv_string(const char* name)
{
  const char* value = std::getenv(name);            // line 621
  ...
}

inline void blend2d_font_resolver::append_env_path(...)
{
  auto value = getenv_string(env_name);
  if (not value.has_value()) { return; }
  std::filesystem::path path(*value);                // line 633
  ...
}

// system_font_directories(), line 646
append_env_path(dirs, "LOCALAPPDATA", std::filesystem::path("Microsoft") / "Windows" / "Fonts");

Two facts collide:

  1. The Windows amd64 wheel is built with MinGW GCC (.github/workflows/wheels.yml puts C:\msys64\mingw64\bin on PATH; CMakeLists.txt has a MinGW branch). libstdc++ on Windows treats every narrow char path string as UTF-8 and throws filesystem_error("Cannot convert character sequence", errc::illegal_byte_sequence) when the bytes are not valid UTF-8 (bits/fs_path.h, __detail::__wstr_from_utf8, path::_Codecvt<wchar_t> = codecvt_utf8_utf16).
  2. The C runtime's getenv returns the variable in the process ANSI code page (CP949 on a Korean-locale machine, cp1252 on most Western ones). LOCALAPPDATA is C:\Users\<username>\AppData\Local, so a non-ASCII username produces bytes that are not UTF-8.

The scan runs eagerly in the threaded renderer constructor (src/pybind/docling_threaded_renderer.h:55, font_resolver_->warm() → build_font_index() → system_font_directories()), which is why the exception surfaces before any page is touched. There is no try/catch around it, so one unconvertible environment variable takes the whole renderer down.

The DOCLING_PARSE_FALLBACK_FONT, DOCLING_PARSE_ARABIC_FALLBACK_FONT and DOCLING_PARSE_CJK_FALLBACK_FONT overrides go through the same getenv_string and have the same problem.

This is not the file-open path: both docling_parser::load_document and docling_threaded_base::load_document already convert the UTF-8 filename to a wide path, and document.h uses std::u8string, so a PDF under C:\Users\한글사용자\... loads fine (the downstream reporter confirmed that too).

Steps to reproduce

Windows amd64, system locale set to Korean (or any non-UTF-8 ANSI code page), logged in as a user whose name contains a character outside ASCII. The "Beta: Use Unicode UTF-8 for worldwide language support" option must be off (it is off by default).

from docling_parse.pdf_parser import DoclingThreadedPdfParser, ParserConfig, RenderConfig
DoclingThreadedPdfParser(ParserConfig(render_config=RenderConfig()))

Same result from DoclingPdfParser on the first PdfPage.render_as_image(...) call.

A cheaper reproduction on any Windows amd64 box is to set LOCALAPPDATA to a value that is not valid UTF-8 before importing:

$env:LOCALAPPDATA = [System.Text.Encoding]::GetEncoding(949).GetString(...)

(or just point it at a directory whose name contains é on a cp1252 machine).

Suggested fix

  1. On _WIN32, read environment variables with _wgetenv (or GetEnvironmentVariableW) and build the path from std::wstring, so the ANSI code page never enters the conversion. Alternatively convert with MultiByteToWideChar(CP_ACP, ...).
  2. Wrap the directory scan in build_font_index() in a try/catch so a bad environment variable degrades to "no system fonts found" instead of aborting the renderer.
  3. Same class of bug on the MSVC (arm64) build, in the other direction: src/pybind/docling_resources.h:47 builds std::filesystem::path __init__path(filename) from the UTF-8 const char* that PyUnicode_AsUTF8 returns, which MSVC interprets as ANSI. That is DoclingPdfParser fails to load resources if user path contains special characters (e.g. á) on Windows #115 (profile path with á, still open). std::u8string there, as document.h already does, fixes both toolchains.

Happy to open a PR for 1 to 3 if that is welcome.

Version

docling-parse: 7.16.0 (downstream report); code unchanged on main at v7.21.0
docling: 2.126.0
Python: 3.12, Windows amd64, Korean system locale

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions