Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c2916f0
#3770 - Enable FreeType support for font manager builds
konstantin-mordovskih Sep 3, 2026
29f648e
#3770 - Add AGENTS.md
konstantin-mordovskih Sep 3, 2026
f53fa85
#3770 - Parse custom fonts from the renderer options JSON
konstantin-mordovskih Sep 6, 2026
f3e3822
Merge branch 'master' into 3770-add-support-for-custom-fonts-in-svg-a…
konstantin-mordovskih Sep 6, 2026
56452d3
Merge branch 'master' into 3770-add-support-for-custom-fonts-in-svg-a…
konstantin-mordovskih Sep 8, 2026
967fa18
Merge
konstantin-mordovskih Sep 8, 2026
f89494d
#3770: Add custom and variable font support to renderer
konstantin-mordovskih Sep 9, 2026
dc2a8bb
Merge branch 'master' into 3770-add-support-for-custom-fonts-in-svg-a…
konstantin-mordovskih Sep 9, 2026
f41c8bb
#3770: Tests have been added
konstantin-mordovskih Sep 14, 2026
d0c8b88
#3770: Apply clang-format
konstantin-mordovskih Sep 14, 2026
e1bccde
#3770: Add custom font fallback and update FreeType
konstantin-mordovskih Sep 17, 2026
2b9a3da
Merge branch 'master' into 3770-add-support-for-custom-fonts-in-svg-a…
konstantin-mordovskih Sep 17, 2026
64c4fdd
#3770: Apply clang-format
konstantin-mordovskih Sep 17, 2026
661b52e
#3770: Tests
konstantin-mordovskih Sep 17, 2026
8ddec40
#3770: Tests
konstantin-mordovskih Sep 17, 2026
9b6420b
#3770: Scope Noto Sans fallback to custom fonts outside Emscripten
konstantin-mordovskih Sep 20, 2026
7b3ad7e
Merge branch 'master' into 3770-add-support-for-custom-fonts-in-svg-a…
konstantin-mordovskih Sep 20, 2026
d435b63
#3770: Skip FreeType init when no custom fonts are supplied
konstantin-mordovskih Sep 20, 2026
56ea207
#3770: Open custom_fonts.ket as UTF-8
konstantin-mordovskih Sep 20, 2026
d3c3ea4
#3770: Add a regression test for text bounding box vs. custom fonts
konstantin-mordovskih Sep 20, 2026
8a2d29f
#3770: Extend custom_fonts test coverage per review
konstantin-mordovskih Sep 20, 2026
95b6cac
#3770: Fix Jython UTF-8 font-test opens, tidy custom-font fixtures
konstantin-mordovskih Sep 20, 2026
6210a86
#3770: Revert FreeType 2.14.3 upgrade
konstantin-mordovskih Sep 21, 2026
d69bce2
#3770: Revert FreeType 2.14.3 upgrade
konstantin-mordovskih Sep 21, 2026
7cd416f
#3770: Bump vendored FreeType's cmake_minimum_required to 3.5
konstantin-mordovskih Sep 21, 2026
0940360
#3770: Regenerate WASM font-rendering references for reverted FreeType
konstantin-mordovskih Sep 21, 2026
3bf42e7
#3770: Disable custom-font FreeType/cairo-ft path under MinGW
konstantin-mordovskih Sep 22, 2026
19d684f
#3770: Force FreeType onto vendored zlib
konstantin-mordovskih Sep 22, 2026
75a47db
#3770: Default to Noto Sans instead of cairo's toy-font API
konstantin-mordovskih Sep 22, 2026
d284518
#3770: Restore text_bounding_box test with a shared reference
konstantin-mordovskih Sep 22, 2026
47700d6
#3770: Pin render-fonts to "[]" in three rendering tests
konstantin-mordovskih Sep 22, 2026
7cf7212
#3770: Reformat two rendering test files with black
konstantin-mordovskih Sep 22, 2026
9810bdd
#3770: Pin render-fonts to "[]" across remaining rendering tests
konstantin-mordovskih Sep 22, 2026
4e90b96
#3770: Take custom fonts by unique_ptr reference in RenderFontFaceMan…
konstantin-mordovskih Sep 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .claude/rules/cpp.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ clang-tidy is configured but **not run** — see [.memory-bank/conventions.md](.
- **C++17.** `cmake/setup.cmake` sets the standard; `std::optional`, `string_view`, structured
bindings and `if constexpr` are available, C++20 constructs are not.
- The same source compiles under GCC, Clang, MSVC and Emscripten. Guard platform code with
`#ifdef _WIN32` / `__APPLE__` / `__linux__` / `EMSCRIPTEN`; under Emscripten there is no `pthread`
and no `cairo`.
`#ifdef _WIN32` / `__APPLE__` / `__linux__` / `EMSCRIPTEN`; under Emscripten there is no `pthread`.
Emscripten forces `BUILD_STANDALONE` on (`CMakeLists.txt`), so `cairo` **is** available there too —
it is the vendored, wasm-compiled copy from `third_party/cairo`, not a system library.
- OS-dependent behaviour goes through the abstractions in `core/indigo-core/common/base_c/` rather
than being written inline a second time.

Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/indigo-ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -945,7 +945,7 @@ jobs:
run: |
mkdir build
cd build
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DRENDER_ENABLE_CJK=ON
emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DRENDER_ENABLE_NOTO_SANS_CJK=ON
cmake --build . --config Release --target indigo-ketcher-package -- -j $(nproc)
- name: Upload artifacts
uses: actions/upload-artifact@v4
Expand Down
4 changes: 2 additions & 2 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ option(WITH_STATIC "Build Indigo static library as well as shared" OFF)

# Indigo API options
option(BUILD_INDIGO "Build indigo shared library" ON)
option(USE_FONT_MANAGER "Build indigo render with font_face_manager" OFF)
option(RENDER_ENABLE_NOTO_SANS "Embed Noto Sans fonts in the renderer" ON)
option(RENDER_ENABLE_NOTO_SANS_CJK "Embed Noto Sans CJK fonts in the renderer" OFF)

# Indigo API Wrappers options
option(BUILD_INDIGO_WRAPPERS "Build all existing Indigo API wrappers" ON)
Expand Down Expand Up @@ -93,7 +94,6 @@ if (EMSCRIPTEN)
set(BUILD_BINGO_SQLSERVER OFF)
set(BUILD_BINGO_ORACLE OFF)
set(BUILD_BINGO_ELASTIC OFF)
set(USE_FONT_MANAGER ON)
# Skip Array/Pool bounds-check throws in release WASM builds to allow inlining.
# emscripten's JS-based exception handling wraps every throw-containing function
# in invoke_* shims, preventing inlining and causing ~80x overhead in hot loops.
Expand Down
131 changes: 131 additions & 0 deletions api/c/indigo-renderer/src/indigo_render2d.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@
#include "indigo_renderer_internal.h"
#include "option_manager.h"

#include <cppcodec/base64_rfc4648.hpp>
#include <freetype/freetype.h>
#include <rapidjson/document.h>
#include <rapidjson/error/en.h>
#include <rapidjson/stringbuffer.h>
#include <rapidjson/writer.h>

#include <cstring>
#include <exception>
#include <memory>
#include <string>
#include <utility>

// #define INDIGO_DEBUG

#ifdef INDIGO_DEBUG
Expand Down Expand Up @@ -145,6 +158,123 @@ void indigoRenderGetOutputFormat(Array<char>& value)
value.readString(mode, true);
}

namespace
{
constexpr size_t MAX_RENDER_FONT_COUNT = 8;
constexpr size_t MAX_RENDER_FONT_BYTES = 16 * 1024 * 1024;
constexpr size_t MAX_RENDER_FONT_TOTAL_BYTES = 32 * 1024 * 1024;
constexpr size_t MAX_RENDER_FONTS_JSON_BYTES = 48 * 1024 * 1024;

bool hasSupportedFontSignature(const std::vector<byte>& data)
{
if (data.size() < 4)
return false;

const byte* signature = data.data();
// TrueType: 0x00010000 or 'true'
// TrueType Collection: 'ttcf'
// OpenType with CFF: 'OTTO'
return (signature[0] == 0 && signature[1] == 1 && signature[2] == 0 && signature[3] == 0) || std::memcmp(signature, "true", 4) == 0 ||
std::memcmp(signature, "ttcf", 4) == 0 || std::memcmp(signature, "OTTO", 4) == 0;
}
} // namespace

void indigoRenderSetFonts(const char* fonts)
Comment thread
NikolaiBalabanov marked this conversation as resolved.
{
if (fonts == nullptr)
throw IndigoError("Invalid fonts JSON: value must not be null");
if (std::strlen(fonts) > MAX_RENDER_FONTS_JSON_BYTES)
throw IndigoError("Invalid fonts JSON: input exceeds %zu bytes", MAX_RENDER_FONTS_JSON_BYTES);

rapidjson::Document document;

document.Parse(fonts);

if (document.HasParseError())
throw IndigoError("Invalid fonts JSON at offset %zu: %s", document.GetErrorOffset(), rapidjson::GetParseError_En(document.GetParseError()));
if (!document.IsArray())
throw IndigoError("Invalid fonts JSON: expected an array");
if (document.Size() > MAX_RENDER_FONT_COUNT)
throw IndigoError("Invalid fonts JSON: too many fonts (maximum %zu)", MAX_RENDER_FONT_COUNT);

auto render_fonts = std::make_unique<std::vector<RenderFont>>();
size_t total_font_bytes = 0;
FT_Library raw_library = nullptr;
if (!document.Empty() && FT_Init_FreeType(&raw_library) != 0)
throw IndigoError("Error initializing FreeType library");
std::unique_ptr<FT_LibraryRec_, decltype(&FT_Done_FreeType)> library(raw_library, FT_Done_FreeType);

for (rapidjson::SizeType i = 0; i < document.Size(); ++i)
{
const auto& font = document[i];

if (!font.IsObject())
throw IndigoError("Invalid font at index %u: expected an object", i);
if (!font.HasMember("name") || !font["name"].IsString())
throw IndigoError("Invalid font at index %u: 'name' must be a string", i);
if (!font.HasMember("data") || !font["data"].IsString())
throw IndigoError("Invalid font at index %u: 'data' must be a Base64 string", i);

std::string font_name(font["name"].GetString(), font["name"].GetStringLength());
const rapidjson::Value& font_base64 = font["data"];
// Base64 encoding increases size by ~4/3: each 3 bytes become 4 characters
if (font_base64.GetStringLength() > ((MAX_RENDER_FONT_BYTES + 2) / 3) * 4)
throw IndigoError("Invalid font at index %u: data exceeds %zu bytes", i, MAX_RENDER_FONT_BYTES);
std::vector<byte> font_data;

try
{
font_data = cppcodec::base64_rfc4648::decode<std::vector<byte>>(font_base64.GetString(), font_base64.GetStringLength());
Comment thread
konstantin-mordovskih marked this conversation as resolved.
}
catch (const std::exception& error)
{
throw IndigoError("Invalid Base64 data for font '%s' at index %u: %s", font_name.c_str(), i, error.what());
}

if (font_data.size() > MAX_RENDER_FONT_BYTES || font_data.size() > MAX_RENDER_FONT_TOTAL_BYTES - total_font_bytes)
throw IndigoError("Invalid font at index %u: font data exceeds size limit", i);
if (!hasSupportedFontSignature(font_data))
throw IndigoError("Invalid font at index %u: unsupported font format", i);

FT_Face raw_face = nullptr;
if (FT_New_Memory_Face(library.get(), font_data.data(), static_cast<FT_Long>(font_data.size()), 0, &raw_face) != 0)
throw IndigoError("Error loading font '%s'", font_name.c_str());
std::unique_ptr<FT_FaceRec_, decltype(&FT_Done_Face)> face(raw_face, FT_Done_Face);
if (FT_Select_Charmap(face.get(), FT_ENCODING_UNICODE) != 0)
throw IndigoError("Font '%s' does not have a Unicode character map", font_name.c_str());

total_font_bytes += font_data.size();
render_fonts->emplace_back(std::move(font_name), std::move(font_data));
}

auto& renderer = indigoRendererGetInstance();

renderer.renderParams.fonts = std::move(render_fonts);
}

void indigoRenderGetFonts(Array<char>& value)
{
rapidjson::StringBuffer buffer;
rapidjson::Writer<rapidjson::StringBuffer> writer(buffer);
writer.StartArray();
const auto& fonts = indigoRendererGetInstance().renderParams.fonts;
if (fonts)
{
for (const RenderFont& font : *fonts)
{
const std::string encoded = cppcodec::base64_rfc4648::encode(*font.data);
writer.StartObject();
writer.Key("name");
writer.String(font.name.c_str(), static_cast<rapidjson::SizeType>(font.name.size()));
writer.Key("data");
writer.String(encoded.c_str(), static_cast<rapidjson::SizeType>(encoded.size()));
writer.EndObject();
}
}
writer.EndArray();
value.readString(buffer.GetString(), true);
}

void indigoRenderSetStereoStyle(const char* mode)
{
std::string mode_string(mode);
Expand Down Expand Up @@ -688,6 +818,7 @@ void IndigoRenderer::setOptionsHandlers()
mgr->setOptionHandlerInt("render-image-max-height", SETTER_GETTER_INT_OPTION(rp.cnvOpt.maxHeight));

mgr->setOptionHandlerString("render-output-format", indigoRenderSetOutputFormat, indigoRenderGetOutputFormat);
mgr->setOptionHandlerString("render-fonts", indigoRenderSetFonts, indigoRenderGetFonts);

mgr->setOptionHandlerString("render-comment", SETTER_GETTER_STR_OPTION(rp.cnvOpt.comment));
mgr->setOptionHandlerString("render-comment-position", indigoRenderSetCommentPosition, indigoRenderGetCommentPosition);
Expand Down
85 changes: 46 additions & 39 deletions api/tests/integration/common/rendering/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,48 +157,55 @@ def imageDiff(imp1, imp2):
# print '%s rendering status: Problem: SVG similarity is %s' % (filename, round(value, 2))


def checkBitmapSimilarity(filename, ref_filename):
def _detect_system():
system = getPlatform()
if system == "mac" or system == "linux":
return system
if os.name == "nt":
return "win"
if os.name == "posix":
return "mac" if platform.mac_ver()[0] else "linux"
if os.name == "java":
osName = System.getProperty("os.name")
if osName.find("Windows") != -1:
return "win"
if osName.find("Linux") != -1:
return "linux"
if osName.find("Mac OS") != -1:
return "mac"
raise RenderingTestException(
"No reference images for this operating system: {0}".format(osName)
)
raise RenderingTestException(
"No reference images for this operating system: {0}".format(os.name)
)


def checkBitmapSimilarity(filename, ref_filename=None, shared=False):
if ref_filename is None:
ref_filename = filename
try:
system = getPlatform()
if system != "mac" and system != "linux":
if os.name == "nt":
system = "win"
elif os.name == "posix":
if not platform.mac_ver()[0]:
system = "linux"
else:
system = "mac"
elif os.name == "java":
osName = System.getProperty("os.name")
if osName.find("Windows") != -1:
system = "win"
elif osName.find("Linux") != -1:
system = "linux"
elif osName.find("Mac OS") != -1:
system = "mac"
else:
raise RenderingTestException(
"No reference images for this operating system: {0}".format(
osName
)
)
else:
raise RenderingTestException(
"No reference images for this operating system: {0}".format(
os.name
)
)
dirname = os.path.normpath(
os.path.abspath(
os.path.join(
os.path.dirname(__file__), "..", "..", "tests", "rendering"
)
dirname = os.path.normpath(
os.path.abspath(
os.path.join(
os.path.dirname(__file__), "..", "..", "tests", "rendering"
)
)
)
try:
if shared:
Comment thread
konstantin-mordovskih marked this conversation as resolved.
# Rendering that never touches OS-native fonts (custom
# embedded fonts, always going through FreeType) produces
# identical pixels on every platform, so it needs one
# reference image, not one per OS.
ref_path = "%s/ref/%s" % (dirname, ref_filename)
else:
ref_path = "%s/ref/%s/%s" % (
dirname,
_detect_system(),
ref_filename,
)
results = imageDiff(
"%s/ref/%s/%s" % (dirname, system, ref_filename),
ref_path,
"%s/out/%s" % (dirname, filename),
)
except RenderingTestException as e:
Expand Down Expand Up @@ -233,9 +240,9 @@ def checkBitmapSimilarity(filename, ref_filename):
return "%s rendering status: OK" % filename


def checkImageSimilarity(filename, ref_filename=None):
def checkImageSimilarity(filename, ref_filename=None, shared=False):
if filename.endswith(".svg"):
# checkSvgSimilarity(filename)
return ""
else:
return checkBitmapSimilarity(filename, ref_filename)
return checkBitmapSimilarity(filename, ref_filename, shared)
1 change: 1 addition & 0 deletions api/tests/integration/ref/rendering/custom_fonts.py.out
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Custom fonts option tests: OK
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Text bounding box tests: OK
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

indigo = Indigo()
renderer = IndigoRenderer(indigo)
indigo.setOption("render-fonts", "[]")

rea = indigo.loadReactionFromFile(
joinPathPy("reactions/catalist_below.ket", __file__)
Expand Down
2 changes: 2 additions & 0 deletions api/tests/integration/tests/rendering/acs_style.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@

indigo = Indigo()
renderer = IndigoRenderer(indigo)
indigo.setOption("render-fonts", "[]")

print("****** Default rendering settings *****")

Expand All @@ -73,6 +74,7 @@

print("****** Issue 2447 wrong stereobond width *****")
indigo.resetOptions()
indigo.setOption("render-fonts", "[]")
indigo.setOption("render-output-format", "png")
indigo.setOption("ignore-stereochemistry-errors", "true")
indigo.setOption("bond-length-unit", "px")
Expand Down
1 change: 1 addition & 0 deletions api/tests/integration/tests/rendering/aligned_grid.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

indigo = Indigo()
renderer = IndigoRenderer(indigo)
indigo.setOption("render-fonts", "[]")

if not os.path.exists(joinPathPy("out", __file__)):
try:
Expand Down
1 change: 1 addition & 0 deletions api/tests/integration/tests/rendering/atom_coloring.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

indigo = Indigo()
renderer = IndigoRenderer(indigo)
indigo.setOption("render-fonts", "[]")

if not os.path.exists(joinPathPy("out", __file__)):
try:
Expand Down
Loading
Loading