diff --git a/.github/workflows/indigo-ci.yaml b/.github/workflows/indigo-ci.yaml index f004e52a31..0989289f11 100644 --- a/.github/workflows/indigo-ci.yaml +++ b/.github/workflows/indigo-ci.yaml @@ -904,6 +904,10 @@ jobs: ver=$(git describe --abbrev=0 --tags --match "indigo-*")-$(git rev-list $(git describe --abbrev=0 --tags --match "indigo-*").. --count) ver=$(echo $ver | sed 's/indigo-//g' | sed 's/-0//g') npm version --allow-same-version ${ver} + - name: Install Puppeteer Dependencies + run: | + apt-get update + apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2 - name: Build, Test, Pack run: | mkdir build @@ -936,6 +940,10 @@ jobs: ver=$(git describe --abbrev=0 --tags --match "indigo-*")-$(git rev-list $(git describe --abbrev=0 --tags --match "indigo-*").. --count) ver=$(echo $ver | sed 's/indigo-//g' | sed 's/-0//g') npm version --allow-same-version ${ver} + - name: Install Puppeteer Dependencies + run: | + apt-get update + apt-get install -y libnss3 libnspr4 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libasound2t64 libpango-1.0-0 libcairo2 fonts-noto-cjk - name: Build, Test, Pack run: | mkdir build @@ -951,7 +959,7 @@ jobs: build_indigo_utils_x86_64: strategy: fail-fast: false - matrix: ${{ fromJSON(needs.set_matrix.outputs.matrix) }} + matrix: ${{ fromJSON(needs.set_matrix.outputs.matrix || '{"os":["ubuntu-latest","windows-latest"]}') }} runs-on: ${{ matrix.os }} # needs: [build_bingo_postgres_linux_x86_64, build_bingo_postgres_windows_msvc_x86_64, build_bingo_postgres_macos_x86_64] needs: [build_bingo_postgres_linux_x86_64, build_bingo_postgres_windows_msvc_x86_64, set_matrix ] diff --git a/CMakeLists.txt b/CMakeLists.txt index 6a93091732..8c7955922d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -93,7 +93,7 @@ if (EMSCRIPTEN) set(BUILD_BINGO_SQLSERVER OFF) set(BUILD_BINGO_ORACLE OFF) set(BUILD_BINGO_ELASTIC OFF) - set(USE_FONT_MANAGER ON) + set(USE_FONT_MANAGER OFF) # 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. diff --git a/api/tests/integration/tests/rendering/ref/win/contracted_fg_ss_mol.png b/api/tests/integration/tests/rendering/ref/win/contracted_fg_ss_mol.png index 99ac9c8510..d72e266e9d 100644 Binary files a/api/tests/integration/tests/rendering/ref/win/contracted_fg_ss_mol.png and b/api/tests/integration/tests/rendering/ref/win/contracted_fg_ss_mol.png differ diff --git a/api/wasm/indigo-ketcher/CMakeLists.txt b/api/wasm/indigo-ketcher/CMakeLists.txt index 78a779fac3..7d7ebc9535 100644 --- a/api/wasm/indigo-ketcher/CMakeLists.txt +++ b/api/wasm/indigo-ketcher/CMakeLists.txt @@ -34,11 +34,14 @@ set(EMCC_COMMON_FLAGS -s INITIAL_MEMORY=32mb -s ALLOW_MEMORY_GROWTH=1 -s DISABLE_EXCEPTION_CATCHING=0 + -s DYNAMIC_EXECUTION=0 + -s ENVIRONMENT=web,worker -s MODULARIZE=1 -s FILESYSTEM=1 ${EMCC_ASSERTIONS} -s USE_SDL=0 -s USE_SDL_IMAGE=0 -s USE_SDL_TTF=0 -s USE_SDL_NET=0 --no-entry + --post-js ${CMAKE_CURRENT_SOURCE_DIR}/post.js ${EMCC_FLAGS} ) @@ -52,15 +55,16 @@ set(EMCC_FLAGS_SEPARATE set(EMCC_FLAGS ${EMCC_COMMON_FLAGS} -s SINGLE_FILE=1 + -s SINGLE_FILE_BINARY_ENCODE=0 ) set(TARGET_FILES $ - $ $ $ $ $ $ $ $ $ $ $ $ $ $ + $ $ $ $ $ $ $ $ ) set(TARGET_FILES_NORENDER - $ $ $ $ $ $ $ $ + $ $ $ $ $ $ $ ) add_custom_target(${PROJECT_NAME}-separate @@ -109,6 +113,7 @@ if (NOT RENDER_ENABLE_CJK) WORKING_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/patch.cmake COMMAND npm test + COMMAND npm run test_browser COMMAND npm pack COMMAND ${CMAKE_COMMAND} -E make_directory ${DIST_DIRECTORY} COMMAND ${COPY_COMMAND} ${CMAKE_LIBRARY_OUTPUT_DIRECTORY}${SEP}${PROJECT_NAME}*.tgz ${NATIVE_DIST_DIRECTORY}${SEP} @@ -125,6 +130,7 @@ else () WORKING_DIRECTORY ${CMAKE_LIBRARY_OUTPUT_DIRECTORY} COMMAND ${CMAKE_COMMAND} -P ${CMAKE_CURRENT_LIST_DIR}/patch.cmake COMMAND npm test + COMMAND npm run test_browser COMMAND npm run test_cjk COMMAND npm pack COMMAND ${CMAKE_COMMAND} -E make_directory ${DIST_DIRECTORY} diff --git a/api/wasm/indigo-ketcher/package.json b/api/wasm/indigo-ketcher/package.json index 5f304a73c2..d315ce20ca 100644 --- a/api/wasm/indigo-ketcher/package.json +++ b/api/wasm/indigo-ketcher/package.json @@ -58,9 +58,21 @@ "homepage": "https://lifescience.opensource.epam.com/indigo/api/index.html", "scripts": { "test": "node test.js", + "test_browser": "node test_browser.js", "test_cjk": "node test_cjk.js" }, "devDependencies": { - "looks-same": "^8.2.4" + "looks-same": "^8.2.4", + "puppeteer": "^22.0.0" + }, + "dependencies": { + "@fontsource/noto-sans": "^5.0.0", + "@fontsource/noto-sans-jp": "^5.0.0", + "@fontsource/noto-sans-kr": "^5.0.0", + "@fontsource/noto-sans-sc": "^5.0.0", + "@fontsource/noto-sans-tc": "^5.0.0" + }, + "optionalDependencies": { + "canvas": "^3.1.0" } } diff --git a/api/wasm/indigo-ketcher/post.js b/api/wasm/indigo-ketcher/post.js new file mode 100644 index 0000000000..4b1a53ea1b --- /dev/null +++ b/api/wasm/indigo-ketcher/post.js @@ -0,0 +1,87 @@ +// post.js - appended to the end of the emscripten module +// Adds Module.renderAsync() for asynchronous SVG-to-PNG conversion + +(function() { + + // Convert SVG string to PNG base64 using browser Canvas API + function svgToPngBrowser(svgStr) { + return new Promise(function(resolve, reject) { + var img = new Image(); + img.onload = function() { + try { + var canvas = document.createElement('canvas'); + canvas.width = img.width; + canvas.height = img.height; + var ctx = canvas.getContext('2d'); + ctx.drawImage(img, 0, 0); + var dataUrl = canvas.toDataURL('image/png'); + resolve(dataUrl.split(',')[1]); + } catch (e) { + reject(e); + } + }; + img.onerror = function() { + reject(new Error('Failed to load SVG into Image for PNG conversion')); + }; + img.src = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(svgStr); + }); + } + + // Convert SVG string to PNG base64 using sharp (Node.js) + function svgToPngNode(svgStr) { + var sharp = require('sharp'); + return sharp(Buffer.from(svgStr)).png().toBuffer().then(function(buf) { + return buf.toString('base64'); + }); + } + + Module.renderAsync = async function(data, options) { + var requestedFormat = 'svg'; + + // Detect requested format and override to SVG for C++ engine + if (options && typeof options.get === 'function') { + try { + var fmt = options.get('render-output-format'); + if (fmt) requestedFormat = fmt; + } catch(e) {} + + if (requestedFormat === 'png') { + options.set('render-output-format', 'svg'); + } + } + + // Call synchronous C++ render — always returns base64-encoded SVG + var base64Out = Module.render(data, options); + + // Restore original option so the caller's map isn't permanently mutated + if (requestedFormat === 'png' && options && typeof options.set === 'function') { + options.set('render-output-format', 'png'); + } + + if (requestedFormat !== 'png') { + return base64Out; + } + + // Decode base64 SVG to string + var svgStr; + if (typeof Buffer !== 'undefined') { + svgStr = Buffer.from(base64Out, 'base64').toString('utf-8'); + } else { + var binary = atob(base64Out); + var bytes = new Uint8Array(binary.length); + for (var i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + svgStr = new TextDecoder().decode(bytes); + } + + // Convert SVG to PNG using the appropriate environment + if (typeof window !== 'undefined' && typeof document !== 'undefined') { + return svgToPngBrowser(svgStr); + } else if (typeof require !== 'undefined') { + return svgToPngNode(svgStr); + } else { + throw new Error('No environment available for SVG to PNG conversion'); + } + }; +})(); diff --git a/api/wasm/indigo-ketcher/test-cjk/CJK_characters_2_styles_2_sizes_ref.png b/api/wasm/indigo-ketcher/test-cjk/CJK_characters_2_styles_2_sizes_ref.png index 605d9d3cd0..411323a3e8 100644 Binary files a/api/wasm/indigo-ketcher/test-cjk/CJK_characters_2_styles_2_sizes_ref.png and b/api/wasm/indigo-ketcher/test-cjk/CJK_characters_2_styles_2_sizes_ref.png differ diff --git a/api/wasm/indigo-ketcher/test-cjk/CJK_characters_ref.png b/api/wasm/indigo-ketcher/test-cjk/CJK_characters_ref.png index eb335f1a7d..79200f6b76 100644 Binary files a/api/wasm/indigo-ketcher/test-cjk/CJK_characters_ref.png and b/api/wasm/indigo-ketcher/test-cjk/CJK_characters_ref.png differ diff --git a/api/wasm/indigo-ketcher/test-cjk/Characters_4_sets_4_styles_ref.png b/api/wasm/indigo-ketcher/test-cjk/Characters_4_sets_4_styles_ref.png index 134107e1c4..31d1d42a52 100644 Binary files a/api/wasm/indigo-ketcher/test-cjk/Characters_4_sets_4_styles_ref.png and b/api/wasm/indigo-ketcher/test-cjk/Characters_4_sets_4_styles_ref.png differ diff --git a/api/wasm/indigo-ketcher/test-cjk/test_cjk.js b/api/wasm/indigo-ketcher/test-cjk/test_cjk.js index 784d3ec7e7..27d8fa0ddd 100644 --- a/api/wasm/indigo-ketcher/test-cjk/test_cjk.js +++ b/api/wasm/indigo-ketcher/test-cjk/test_cjk.js @@ -13,15 +13,42 @@ function parseHrtimeToSeconds(hrtime) { return (hrtime[0] + (hrtime[1] / 1e9)).toFixed(3); } -function run() { +async function run() { let succeeded = 0; let failed = 0; + + // On Linux, configure FontConfig to use the @fontsource fonts so that `sharp` (librsvg) + // renders SVGs with the correct Noto Sans CJK fonts, avoiding looksSame failures in CI. + if (process.platform === 'linux') { + const { execSync } = require('child_process'); + const os = require('os'); + const path = require('path'); + const fs = require('fs'); + try { + console.log("Configuring FontConfig for Linux..."); + const fontsDir = path.join(os.homedir(), '.fonts'); + if (!fs.existsSync(fontsDir)) { + fs.mkdirSync(fontsDir, { recursive: true }); + } + const sourceDir = path.join(__dirname, 'node_modules', '@fontsource'); + if (fs.existsSync(sourceDir)) { + execSync(`find "${sourceDir}" -type f \\( -name "*.woff" -o -name "*.woff2" -o -name "*.ttf" \\) -exec cp {} "${fontsDir}" \\;`); + execSync('fc-cache -f -v'); + console.log("FontConfig successfully updated with Noto Sans fonts."); + } else { + console.warn(`Fontsource directory not found at ${sourceDir}`); + } + } catch (e) { + console.error('Failed to configure FontConfig for Linux:', e.message); + } + } + console.log("Starting tests...\n") var startTestsTime = process.hrtime(); - tests.forEach(t => { + for (const t of tests) { try { var startTestTime = process.hrtime(); - t.fn() + await t.fn() const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTestTime)); console.log(`✅ ${t.group}.${t.name} [${elapsedSeconds}s]`); succeeded++; @@ -30,7 +57,7 @@ function run() { console.log(e.stack) failed++ } - }) + } const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTestsTime)); const total = succeeded + failed; console.log(`\n${total} tests executed in ${elapsedSeconds} seconds. ${succeeded} succeeded, ${failed} failed.`) diff --git a/api/wasm/indigo-ketcher/test/CJK_characters_2_styles_2_sizes_ref.png b/api/wasm/indigo-ketcher/test/CJK_characters_2_styles_2_sizes_ref.png new file mode 100644 index 0000000000..411323a3e8 Binary files /dev/null and b/api/wasm/indigo-ketcher/test/CJK_characters_2_styles_2_sizes_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/CJK_characters_ref.png b/api/wasm/indigo-ketcher/test/CJK_characters_ref.png new file mode 100644 index 0000000000..79200f6b76 Binary files /dev/null and b/api/wasm/indigo-ketcher/test/CJK_characters_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/Characters_4_sets_4_styles_ref.png b/api/wasm/indigo-ketcher/test/Characters_4_sets_4_styles_ref.png new file mode 100644 index 0000000000..31d1d42a52 Binary files /dev/null and b/api/wasm/indigo-ketcher/test/Characters_4_sets_4_styles_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/bonds.mol b/api/wasm/indigo-ketcher/test/bonds.mol new file mode 100644 index 0000000000..ee70304bdb --- /dev/null +++ b/api/wasm/indigo-ketcher/test/bonds.mol @@ -0,0 +1,45 @@ + + Mrv0541 12021111492D + + 26 13 0 0 0 0 999 V2000 + 0.4125 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4125 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.4768 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6518 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 6.4232 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 5.5982 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 9.4875 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 8.6625 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.4125 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4125 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.4768 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.6518 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 6.5411 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 5.7161 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 9.6054 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 8.7804 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.4125 -3.3000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4125 -3.3000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 3.2411 -3.3000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 2.4161 -3.3000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 6.0696 -3.3000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 5.2446 -3.3000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 8.8982 -3.3000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 8.0732 -3.3000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 0.4125 -4.9500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + -0.4125 -4.9500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0 + 1 2 1 0 0 0 0 + 3 4 2 0 0 0 0 + 5 6 3 0 0 0 0 + 7 8 4 0 0 0 0 + 9 10 1 1 0 0 0 + 11 12 1 6 0 0 0 + 13 14 1 4 0 0 0 + 15 16 2 3 0 0 0 + 17 18 2 3 0 0 0 + 19 20 5 0 0 0 0 + 21 22 6 0 0 0 0 + 23 24 7 0 0 0 0 + 25 26 8 0 0 0 0 +M MRV CTU 1 9 +M END diff --git a/api/wasm/indigo-ketcher/test/bonds_ref.png b/api/wasm/indigo-ketcher/test/bonds_ref.png new file mode 100644 index 0000000000..8e1e9c9e74 Binary files /dev/null and b/api/wasm/indigo-ketcher/test/bonds_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/issue_2513.ket b/api/wasm/indigo-ketcher/test/issue_2513.ket new file mode 100644 index 0000000000..07c0ccfcc3 --- /dev/null +++ b/api/wasm/indigo-ketcher/test/issue_2513.ket @@ -0,0 +1,102 @@ +{ + "root": { + "nodes": [ + { + "$ref": "mol0" + }, + { + "$ref": "mol1" + }, + { + "$ref": "mol2" + }, + { + "type": "arrow", + "data": { + "mode": "elliptical-arc-arrow-open-angle", + "pos": [ + { + "x": 5.799999999999997, + "y": -8.200000000000003, + "z": 0 + }, + { + "x": 7.205569279687058, + "y": -8.200000000000003, + "z": 0 + } + ], + "height": 1 + } + }, + { + "type": "arrow", + "data": { + "mode": "open-angle", + "pos": [ + { + "x": 7.775, + "y": -8.225, + "z": 0 + }, + { + "x": 8.875, + "y": -8.225, + "z": 0 + } + ] + } + }, + { + "type": "plus", + "location": [ + 6.525, + -8.225, + 0 + ], + "prop": {} + } + ], + "connections": [], + "templates": [] + }, + "mol0": { + "type": "molecule", + "atoms": [ + { + "label": "N", + "location": [ + 5.749999999999999, + -8.350000000000001, + 0 + ] + } + ] + }, + "mol1": { + "type": "molecule", + "atoms": [ + { + "label": "O", + "location": [ + 7.3999999999999995, + -8.3, + 0 + ] + } + ] + }, + "mol2": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 9.15, + -8.3, + 0 + ] + } + ] + } +} \ No newline at end of file diff --git a/api/wasm/indigo-ketcher/test/issue_2513_ref.png b/api/wasm/indigo-ketcher/test/issue_2513_ref.png new file mode 100644 index 0000000000..f0ca71b439 Binary files /dev/null and b/api/wasm/indigo-ketcher/test/issue_2513_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/ketcher_elliptical_arrow_ref.png b/api/wasm/indigo-ketcher/test/ketcher_elliptical_arrow_ref.png index 7fc410a90e..cc8af6ce54 100644 Binary files a/api/wasm/indigo-ketcher/test/ketcher_elliptical_arrow_ref.png and b/api/wasm/indigo-ketcher/test/ketcher_elliptical_arrow_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/ketcher_text_panel_bold_italic_ref.png b/api/wasm/indigo-ketcher/test/ketcher_text_panel_bold_italic_ref.png index ac4c5696f8..b00fe91d51 100644 Binary files a/api/wasm/indigo-ketcher/test/ketcher_text_panel_bold_italic_ref.png and b/api/wasm/indigo-ketcher/test/ketcher_text_panel_bold_italic_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/ketcher_text_panel_bold_ref.png b/api/wasm/indigo-ketcher/test/ketcher_text_panel_bold_ref.png index 06aba97bad..71b5987b9f 100644 Binary files a/api/wasm/indigo-ketcher/test/ketcher_text_panel_bold_ref.png and b/api/wasm/indigo-ketcher/test/ketcher_text_panel_bold_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/ketcher_text_panel_italic_ref.png b/api/wasm/indigo-ketcher/test/ketcher_text_panel_italic_ref.png index 3ad73f96ec..3c9bfe0b5e 100644 Binary files a/api/wasm/indigo-ketcher/test/ketcher_text_panel_italic_ref.png and b/api/wasm/indigo-ketcher/test/ketcher_text_panel_italic_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/ketcher_text_panel_regular_ref.png b/api/wasm/indigo-ketcher/test/ketcher_text_panel_regular_ref.png index a7b89c3622..6419bb26ae 100644 Binary files a/api/wasm/indigo-ketcher/test/ketcher_text_panel_regular_ref.png and b/api/wasm/indigo-ketcher/test/ketcher_text_panel_regular_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/multi.ket b/api/wasm/indigo-ketcher/test/multi.ket new file mode 100644 index 0000000000..9ed02a53cd --- /dev/null +++ b/api/wasm/indigo-ketcher/test/multi.ket @@ -0,0 +1,1059 @@ +{ + "root": { + "nodes": [ + { + "$ref": "mol0" + }, + { + "$ref": "mol1" + }, + { + "$ref": "mol2" + }, + { + "$ref": "mol3" + }, + { + "$ref": "mol4" + }, + { + "$ref": "mol5" + }, + { + "$ref": "mol6" + }, + { + "$ref": "mol7" + }, + { + "type": "arrow", + "data": { + "mode": "open-angle", + "pos": [ + { + "x": 12.325000000000001, + "y": -3.575, + "z": 0 + }, + { + "x": 16.075, + "y": -3.5500000000000003, + "z": 0 + } + ] + } + }, + { + "type": "arrow", + "data": { + "mode": "open-angle", + "pos": [ + { + "x": 20.325000000000003, + "y": -4.825, + "z": 0 + }, + { + "x": 20.375, + "y": -8.175, + "z": 0 + } + ] + } + }, + { + "type": "arrow", + "data": { + "mode": "open-angle", + "pos": [ + { + "x": 16.675, + "y": -9.5, + "z": 0 + }, + { + "x": 12.725000000000001, + "y": -9.525, + "z": 0 + } + ] + } + }, + { + "type": "arrow", + "data": { + "mode": "open-angle", + "pos": [ + { + "x": 7.575, + "y": -8.025, + "z": 0 + }, + { + "x": 5.375, + "y": -4.825, + "z": 0 + } + ] + } + }, + { + "type": "plus", + "location": [ + 4.375000000000001, + -3.725, + 0 + ], + "prop": {} + }, + { + "type": "plus", + "location": [ + 18.900000000000002, + -3.45, + 0 + ], + "prop": {} + }, + { + "type": "plus", + "location": [ + 20.2, + -9.375, + 0 + ], + "prop": {} + }, + { + "type": "plus", + "location": [ + 6.45, + -9.725000000000001, + 0 + ], + "prop": {} + }, + { + "type": "simpleObject", + "data": { + "mode": "ellipse", + "pos": [ + { + "x": 11.525, + "y": -5.275, + "z": 0 + }, + { + "x": 19.275000000000002, + "y": -8.025, + "z": 0 + } + ] + } + }, + { + "type": "simpleObject", + "data": { + "mode": "rectangle", + "pos": [ + { + "x": 13.975000000000001, + "y": -6.125, + "z": 0 + }, + { + "x": 18.125, + "y": -4.675, + "z": 0 + } + ] + } + }, + { + "type": "simpleObject", + "data": { + "mode": "ellipse", + "pos": [ + { + "x": 22.8, + "y": -2.75, + "z": 0 + }, + { + "x": 24.875, + "y": -7.25, + "z": 0 + } + ] + } + }, + { + "type": "simpleObject", + "data": { + "mode": "line", + "pos": [ + { + "x": 12.450000000000001, + "y": -4.4750000000000005, + "z": 0 + }, + { + "x": 18.900000000000002, + "y": -8.05, + "z": 0 + } + ] + } + }, + { + "type": "simpleObject", + "data": { + "mode": "line", + "pos": [ + { + "x": 25.325000000000003, + "y": -3.0500000000000003, + "z": 0 + }, + { + "x": 10.15, + "y": 0.17500000000000002, + "z": 0 + } + ] + } + } + ] + }, + "mol0": { + "type": "molecule", + "atoms": [ + { + "label": "O", + "location": [ + 10.343386646282486, + -2.31720847428936, + 0 + ] + }, + { + "label": "O", + "location": [ + 10.343386646282486, + -4.192297187075282, + 0 + ] + }, + { + "label": "O", + "location": [ + 8.52399759820894, + -5.1327915257106405, + 0 + ] + }, + { + "label": "O", + "location": [ + 6.704508550737346, + -4.19239718647333, + 0 + ] + }, + { + "label": "O", + "location": [ + 8.518397631918287, + -2.31720847428936, + 0 + ] + }, + { + "label": "O", + "location": [ + 5.906613353717513, + -2.911504896884781, + 0 + ] + }, + { + "label": "C", + "location": [ + 9.433592122847665, + -2.7763057107247593, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 9.433592122847665, + -3.7276999837472786, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 8.518397631918287, + -4.19239718647333, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 7.608803107279562, + -3.7278999825433736, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 7.608803107279562, + -2.776405710122807, + 0 + ], + "stereoLabel": "abs" + }, + { + "label": "C", + "location": [ + 6.704508550737346, + -2.3174084730854547, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 5, + 11 + ] + }, + { + "type": 1, + "atoms": [ + 10, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 4, + 6 + ] + }, + { + "type": 1, + "atoms": [ + 6, + 7 + ] + }, + { + "type": 1, + "atoms": [ + 7, + 8 + ] + }, + { + "type": 1, + "atoms": [ + 8, + 9 + ] + }, + { + "type": 1, + "atoms": [ + 9, + 10 + ] + }, + { + "type": 1, + "atoms": [ + 10, + 11 + ], + "stereo": 1 + }, + { + "type": 1, + "atoms": [ + 6, + 0 + ], + "stereo": 6 + }, + { + "type": 1, + "atoms": [ + 7, + 1 + ], + "stereo": 6 + }, + { + "type": 1, + "atoms": [ + 8, + 2 + ], + "stereo": 6 + }, + { + "type": 1, + "atoms": [ + 9, + 3 + ], + "stereo": 6 + } + ] + }, + "mol1": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 3.1572074547389786, + -4.197548743464718, + 0 + ] + }, + { + "label": "C", + "location": [ + 3.1535041146649103, + -3.2024512565352814, + 0 + ] + }, + { + "label": "C", + "location": [ + 2.1452948020678244, + -4.184637098341615, + 0 + ] + }, + { + "label": "C", + "location": [ + 2.1427925452610217, + -3.2024512565352814, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 3 + ] + } + ], + "stereoFlagPosition": { + "x": 3.1572074547389786, + "y": -2.2024512565352814, + "z": 0 + } + }, + "mol2": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 17.882207454738978, + -4.022548743464718, + 0 + ] + }, + { + "label": "C", + "location": [ + 17.87850411466491, + -3.0274512565352816, + 0 + ] + }, + { + "label": "C", + "location": [ + 16.870294802067825, + -4.009637098341615, + 0 + ] + }, + { + "label": "C", + "location": [ + 16.867792545261022, + -3.0274512565352816, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 3 + ] + } + ], + "stereoFlagPosition": { + "x": 17.882207454738978, + "y": -2.0274512565352816, + "z": 0 + } + }, + "mol3": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 19.82437421436393, + -3.857470415521406, + 0 + ] + }, + { + "label": "C", + "location": [ + 20.825625785636074, + -3.857470415521406, + 0 + ] + }, + { + "label": "C", + "location": [ + 20.32505018804869, + -2.992529584478595, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + } + ] + }, + "mol4": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 19.257207454738978, + -9.97254874346472, + 0 + ] + }, + { + "label": "C", + "location": [ + 19.25350411466491, + -8.977451256535282, + 0 + ] + }, + { + "label": "C", + "location": [ + 18.245294802067825, + -9.959637098341616, + 0 + ] + }, + { + "label": "C", + "location": [ + 18.242792545261022, + -8.977451256535282, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 0, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 3 + ] + } + ], + "stereoFlagPosition": { + "x": 19.257207454738978, + "y": -7.977451256535282, + "z": 0 + } + }, + "mol5": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 21.825000000000003, + -8.505559548551435, + 0 + ] + }, + { + "label": "C", + "location": [ + 22.633989960649675, + -9.093352254200232, + 0 + ] + }, + { + "label": "C", + "location": [ + 22.324993795209934, + -10.044440451448565, + 0 + ] + }, + { + "label": "C", + "location": [ + 21.32500620479007, + -10.044440451448565, + 0 + ] + }, + { + "label": "C", + "location": [ + 21.01601003935033, + -9.093352254200232, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 4, + 0 + ] + } + ], + "stereoFlagPosition": { + "x": 22.633989960649675, + "y": -7.505559548551435, + "z": 0 + } + }, + "mol6": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 11.594976523224318, + -10.097793451433898, + 0 + ] + }, + { + "label": "C", + "location": [ + 11.596076507988652, + -9.095507333895412, + 0 + ] + }, + { + "label": "O", + "location": [ + 10.622989986012497, + -8.88071030900539, + 0 + ] + }, + { + "label": "C", + "location": [ + 10.389493220128793, + -7.916523663758851, + 0 + ] + }, + { + "label": "C", + "location": [ + 9.38880708042934, + -7.925223543258587, + 0 + ] + }, + { + "label": "O", + "location": [ + 9.172410077700286, + -8.894610116481982, + 0 + ] + }, + { + "label": "C", + "location": [ + 8.203923492011349, + -9.12920686713002, + 0 + ] + }, + { + "label": "C", + "location": [ + 8.203923492011349, + -10.136992908490175, + 0 + ] + }, + { + "label": "O", + "location": [ + 9.17181008601065, + -10.362389786563782, + 0 + ] + }, + { + "label": "C", + "location": [ + 9.393507015331494, + -11.333476336241148, + 0 + ] + }, + { + "label": "C", + "location": [ + 10.393393166111432, + -11.319076535689863, + 0 + ] + }, + { + "label": "O", + "location": [ + 10.61259013005879, + -10.331990207622177, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 4, + 5 + ] + }, + { + "type": 1, + "atoms": [ + 5, + 6 + ] + }, + { + "type": 1, + "atoms": [ + 6, + 7 + ] + }, + { + "type": 1, + "atoms": [ + 7, + 8 + ] + }, + { + "type": 1, + "atoms": [ + 8, + 9 + ] + }, + { + "type": 1, + "atoms": [ + 9, + 10 + ] + }, + { + "type": 1, + "atoms": [ + 10, + 11 + ] + }, + { + "type": 1, + "atoms": [ + 11, + 0 + ] + } + ], + "stereoFlagPosition": { + "x": 11.596076507988652, + "y": -6.916523663758851, + "z": 0 + } + }, + "mol7": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 4.350000000000002, + -8.930559548551436, + 0 + ] + }, + { + "label": "C", + "location": [ + 5.15898996064967, + -9.518352254200233, + 0 + ] + }, + { + "label": "C", + "location": [ + 4.849993795209931, + -10.469440451448566, + 0 + ] + }, + { + "label": "C", + "location": [ + 3.850006204790067, + -10.469440451448566, + 0 + ] + }, + { + "label": "C", + "location": [ + 3.5410100393503288, + -9.518352254200233, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 2, + 3 + ] + }, + { + "type": 1, + "atoms": [ + 3, + 4 + ] + }, + { + "type": 1, + "atoms": [ + 4, + 0 + ] + } + ], + "stereoFlagPosition": { + "x": 5.15898996064967, + "y": -7.930559548551436, + "z": 0 + } + } +} \ No newline at end of file diff --git a/api/wasm/indigo-ketcher/test/multi_reaction_ref.png b/api/wasm/indigo-ketcher/test/multi_reaction_ref.png new file mode 100644 index 0000000000..1ce3e45498 Binary files /dev/null and b/api/wasm/indigo-ketcher/test/multi_reaction_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/multitail_arrow_ref.png b/api/wasm/indigo-ketcher/test/multitail_arrow_ref.png new file mode 100644 index 0000000000..987e3151c6 Binary files /dev/null and b/api/wasm/indigo-ketcher/test/multitail_arrow_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/pathway11_ref.png b/api/wasm/indigo-ketcher/test/pathway11_ref.png new file mode 100644 index 0000000000..646661f06f Binary files /dev/null and b/api/wasm/indigo-ketcher/test/pathway11_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/retro.ket b/api/wasm/indigo-ketcher/test/retro.ket new file mode 100644 index 0000000000..0b11285f35 --- /dev/null +++ b/api/wasm/indigo-ketcher/test/retro.ket @@ -0,0 +1,137 @@ +{ + "root": { + "nodes": [ + { + "$ref": "mol0" + }, + { + "$ref": "mol1" + }, + { + "type": "arrow", + "data": { + "mode": "retrosynthetic", + "pos": [ + { + "x": 17.023752881253245, + "y": -8.175, + "z": 0 + }, + { + "x": 18.750181019051478, + "y": -8.175, + "z": 0 + } + ] + } + } + ], + "connections": [], + "templates": [] + }, + "mol0": { + "type": "molecule", + "atoms": [ + { + "label": "C", + "location": [ + 15.246467820890256, + -9.074998653426483, + 0 + ] + }, + { + "label": "C", + "location": [ + 15.246467820890256, + -8.075000455066647, + 0 + ] + }, + { + "label": "C", + "location": [ + 14.38044431997395, + -7.5750013465735195, + 0 + ] + }, + { + "label": "N", + "location": [ + 16.11249117279522, + -7.5750013465735195, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 3 + ] + } + ] + }, + "mol1": { + "type": "molecule", + "atoms": [ + { + "label": "O", + "location": [ + 19.987496757286365, + -8.075000455066647, + 0 + ] + }, + { + "label": "C", + "location": [ + 20.853495224297802, + -8.574999544933355, + 0 + ] + }, + { + "label": "O", + "location": [ + 21.71955568002605, + -8.075000455066647, + 0 + ] + } + ], + "bonds": [ + { + "type": 1, + "atoms": [ + 0, + 1 + ] + }, + { + "type": 1, + "atoms": [ + 1, + 2 + ] + } + ] + } +} \ No newline at end of file diff --git a/api/wasm/indigo-ketcher/test/retro_arrow_ref.png b/api/wasm/indigo-ketcher/test/retro_arrow_ref.png new file mode 100644 index 0000000000..8a0cbd0acc Binary files /dev/null and b/api/wasm/indigo-ketcher/test/retro_arrow_ref.png differ diff --git a/api/wasm/indigo-ketcher/test/test.js b/api/wasm/indigo-ketcher/test/test.js index 4ac7344e09..6f280db677 100644 --- a/api/wasm/indigo-ketcher/test/test.js +++ b/api/wasm/indigo-ketcher/test/test.js @@ -1,7 +1,7 @@ const indigoModuleFn = require('./indigo-ketcher.js') const assert = require('assert').strict; -const looksSame = require('looks-same'); - +const looksSameOrig = require('looks-same'); +const looksSame = async (ref, out) => await looksSameOrig(ref, out, { tolerance: 5, antialiasingTolerance: 5, ignoreAntialiasing: true }); // Extremely simple test framework, thanks to @sohamkamari (https://github.com/sohamkamani/nodejs-test-without-library) let tests = [] @@ -13,15 +13,42 @@ function parseHrtimeToSeconds(hrtime) { return (hrtime[0] + (hrtime[1] / 1e9)).toFixed(3); } -function run() { +async function run() { let succeeded = 0; let failed = 0; + + // On Linux, configure FontConfig to use the @fontsource fonts so that `sharp` (librsvg) + // renders SVGs with the correct Noto Sans fonts, avoiding looksSame failures in CI. + if (process.platform === 'linux') { + const { execSync } = require('child_process'); + const os = require('os'); + const path = require('path'); + const fs = require('fs'); + try { + console.log("Configuring FontConfig for Linux..."); + const fontsDir = path.join(os.homedir(), '.fonts'); + if (!fs.existsSync(fontsDir)) { + fs.mkdirSync(fontsDir, { recursive: true }); + } + const sourceDir = path.join(__dirname, 'node_modules', '@fontsource'); + if (fs.existsSync(sourceDir)) { + execSync(`find "${sourceDir}" -type f \\( -name "*.woff" -o -name "*.woff2" -o -name "*.ttf" \\) -exec cp {} "${fontsDir}" \\;`); + execSync('fc-cache -f -v'); + console.log("FontConfig successfully updated with Noto Sans fonts."); + } else { + console.warn(`Fontsource directory not found at ${sourceDir}`); + } + } catch (e) { + console.error('Failed to configure FontConfig for Linux:', e.message); + } + } + console.log("Starting tests...\n") var startTestsTime = process.hrtime(); - tests.forEach(t => { + for (const t of tests) { try { var startTestTime = process.hrtime(); - t.fn() + await t.fn() const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTestTime)); console.log(`✅ ${t.group}.${t.name} [${elapsedSeconds}s]`); succeeded++; @@ -30,7 +57,7 @@ function run() { console.log(e.stack) failed++ } - }) + } const elapsedSeconds = parseHrtimeToSeconds(process.hrtime(startTestsTime)); const total = succeeded + failed; console.log(`\n${total} tests executed in ${elapsedSeconds} seconds. ${succeeded} succeeded, ${failed} failed.`) @@ -660,6 +687,63 @@ M END options.delete(); }); + test("render", "svg_arc_commands", () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "svg"); + var fs = require('fs'); + var path = require('path'); + const ket_data = fs.readFileSync(path.join(__dirname, "issue_2513.ket")); + const svg = Buffer.from(indigo.render(ket_data, options), "base64").toString(); + assert(/ { + var fs = require('fs'); + var path = require('path'); + let currentDir = __dirname; + while (currentDir !== path.parse(currentDir).root) { + let candidate = path.join(currentDir, "api/tests/integration/tests", subpath); + if (fs.existsSync(candidate)) return candidate; + currentDir = path.dirname(currentDir); + } + throw new Error("Could not find integration test file: " + subpath); + }; + + test("render", "embedded_images_svg", () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "svg"); + var fs = require('fs'); + const ket_data = fs.readFileSync(getIntegrationTestPath("rendering/molecules/images.ket")); + const svg = Buffer.from(indigo.render(ket_data, options), "base64").toString(); + assert(svg.indexOf(" { + let options = new indigo.MapStringString(); + options.set("render-output-format", "svg"); + var fs = require('fs'); + const cdxml_data = fs.readFileSync(getIntegrationTestPath("formats/ref/images.cdxml")); + const svg = Buffer.from(indigo.render(cdxml_data, options), "base64").toString(); + assert(svg.indexOf(" { + let options = new indigo.MapStringString(); + options.set("render-output-format", "svg"); + var fs = require('fs'); + const cdx_data = fs.readFileSync(getIntegrationTestPath("formats/molecules/cdx/image.cdx")); + const svg = Buffer.from(indigo.render("base64::" + cdx_data.toString("base64"), options), "base64").toString(); + assert(svg.indexOf(" { let options = new indigo.MapStringString(); options.set("render-output-format", "png"); @@ -773,6 +857,97 @@ M END assert(equal); options.delete(); }); + + test("render", "multi_reaction", async () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "png"); + options.set("render-background-color", "1,1,1"); + var fs = require('fs'); + const ket_data = fs.readFileSync("multi.ket"); + const png = Buffer.from(indigo.render(ket_data, options), "base64"); + fs.writeFileSync("multi_reaction_out.png", png); + const { equal } = await looksSame('multi_reaction_ref.png', 'multi_reaction_out.png'); + assert(equal); + options.delete(); + }); + + test("render", "retro_arrow", async () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "png"); + options.set("render-background-color", "1,1,1"); + var fs = require('fs'); + const ket_data = fs.readFileSync("retro.ket"); + const png = Buffer.from(indigo.render(ket_data, options), "base64"); + fs.writeFileSync("retro_arrow_out.png", png); + const { equal } = await looksSame('retro_arrow_ref.png', 'retro_arrow_out.png'); + assert(equal); + options.delete(); + }); + + test("render", "issue_2513_elliptical_arc", async () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "png"); + options.set("render-background-color", "1,1,1"); + var fs = require('fs'); + const ket_data = fs.readFileSync("issue_2513.ket"); + const png = Buffer.from(indigo.render(ket_data, options), "base64"); + fs.writeFileSync("issue_2513_out.png", png); + const { equal } = await looksSame('issue_2513_ref.png', 'issue_2513_out.png'); + assert(equal); + options.delete(); + }); + + test("render", "bonds", async () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "png"); + options.set("render-background-color", "1,1,1"); + var fs = require('fs'); + const mol_data = fs.readFileSync("bonds.mol"); + const png = Buffer.from(indigo.render(mol_data, options), "base64"); + fs.writeFileSync("bonds_out.png", png); + const { equal } = await looksSame('bonds_ref.png', 'bonds_out.png'); + assert(equal); + options.delete(); + }); + + test("render", "multitail_arrow", async () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "png"); + options.set("render-background-color", "1,1,1"); + var fs = require('fs'); + const ket_data = fs.readFileSync(getIntegrationTestPath("rendering/reactions/multitail_arrow.ket")); + const png = Buffer.from(indigo.render(ket_data, options), "base64"); + fs.writeFileSync("multitail_arrow_out.png", png); + const { equal } = await looksSame('multitail_arrow_ref.png', 'multitail_arrow_out.png'); + assert(equal); + options.delete(); + }); + + test("render", "pathway11_multitail_text", async () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "png"); + options.set("render-background-color", "1,1,1"); + var fs = require('fs'); + const ket_data = fs.readFileSync(getIntegrationTestPath("formats/reactions/pathway11.ket")); + const png = Buffer.from(indigo.render(ket_data, options), "base64"); + fs.writeFileSync("pathway11_out.png", png); + const { equal } = await looksSame('pathway11_ref.png', 'pathway11_out.png'); + assert(equal); + options.delete(); + }); + + test("render", "text_test5", async () => { + let options = new indigo.MapStringString(); + options.set("render-output-format", "png"); + options.set("render-background-color", "1,1,1"); + var fs = require('fs'); + const ket_data = fs.readFileSync("text_test5.ket"); + const png = Buffer.from(indigo.render(ket_data, options), "base64"); + fs.writeFileSync("text_test5_out.png", png); + const { equal } = await looksSame('text_test5_ref.png', 'text_test5_out.png'); + assert(equal); + options.delete(); + }); } // Throws diff --git a/api/wasm/indigo-ketcher/test/test_browser.js b/api/wasm/indigo-ketcher/test/test_browser.js new file mode 100644 index 0000000000..6bdbdad4cb --- /dev/null +++ b/api/wasm/indigo-ketcher/test/test_browser.js @@ -0,0 +1,105 @@ +const puppeteer = require('puppeteer'); +const path = require('path'); +const fs = require('fs'); + +(async () => { + let browser; + try { + console.log("Starting browser test for renderAsync..."); + + // Ensure indigo-ketcher.js is available (it should be copied to the current directory by CMake during tests) + const scriptPath = path.join(__dirname, 'indigo-ketcher.js'); + if (!fs.existsSync(scriptPath)) { + console.error("❌ Cannot find indigo-ketcher.js in " + __dirname); + process.exit(1); + } + + browser = await puppeteer.launch({ + headless: 'new', + args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'] + }); + + const page = await browser.newPage(); + + // Pipe page console to node console + page.on('console', msg => console.log('Browser log:', msg.text())); + + // We will expose a resolve/reject pair so the page context can communicate back to Node context + const testPromise = new Promise(async (resolve, reject) => { + await page.exposeFunction('reportResult', (success, message) => { + if (success) { + console.log('✅ ' + message); + resolve(); + } else { + console.error('❌ ' + message); + reject(new Error(message)); + } + }); + }); + + // Add the WASM bundle script tag + await page.addScriptTag({ path: scriptPath }); + + // Run the test in the browser context + await page.evaluate(async () => { + try { + // Determine the exported module factory name + const moduleFactory = window.Module || window.indigoKetcher; + if (typeof moduleFactory !== 'function') { + throw new Error("Could not find Module or indigoKetcher global factory function"); + } + + console.log("Instantiating WASM module..."); + const indigo = await moduleFactory(); + console.log("WASM module loaded successfully."); + + if (typeof indigo.renderAsync !== 'function') { + throw new Error("renderAsync method is missing on the instantiated module!"); + } + + // Render a simple molecule to PNG using the browser pipeline (Canvas) + const options = new indigo.MapStringString(); + options.set("render-output-format", "png"); + + const b64 = await indigo.renderAsync("C1=CC=CC=C1", options); + options.delete(); + + if (!b64 || typeof b64 !== 'string') { + throw new Error("renderAsync returned invalid output type: " + typeof b64); + } + + // PNG magic bytes in base64 are "iVBORw0KGgo" + if (!b64.startsWith("iVBORw0K")) { + throw new Error("PNG magic bytes validation failed. Output starts with: " + b64.substring(0, 30)); + } + + // Test SVG format via renderAsync just to ensure it works + const optionsSvg = new indigo.MapStringString(); + optionsSvg.set("render-output-format", "svg"); + const svgOut = await indigo.renderAsync("C1=CC=CC=C1", optionsSvg); + optionsSvg.delete(); + + if (typeof window.atob === 'function') { + const decoded = window.atob(svgOut); + if (decoded.indexOf(' #include #include @@ -342,3 +343,33 @@ std::string indigo::createEMFFromBitmap(const std::string& bmpData) emfData.replace(0, sizeof(emfHeader), reinterpret_cast(&emfHeader), sizeof(emfHeader)); return emfData; } +#endif // !__EMSCRIPTEN__ + +#ifdef __EMSCRIPTEN__ +#include "emf_utils.h" +#include +#include + +namespace indigo +{ + std::string dibToPNG(const std::string& /*dib_data*/) + { + return ""; + } + + std::vector ripBitmapsFromEMF(const std::string& /*emf*/) + { + return {}; + } + + std::string decodePNG(const std::string& /*inputPNGData*/) + { + return ""; + } + + std::string createEMFFromBitmap(const std::string& /*bmpData*/) + { + return ""; + } +} +#endif diff --git a/core/render2d/CMakeLists.txt b/core/render2d/CMakeLists.txt index 9fb3f7554c..ccf0b2d6ca 100644 --- a/core/render2d/CMakeLists.txt +++ b/core/render2d/CMakeLists.txt @@ -13,6 +13,9 @@ file(GLOB ${PROJECT_NAME}_SOURCES CONFIGURE_DEPENDS if (EMSCRIPTEN) add_definitions(-DRENDER_EMSCRIPTEN) + # On WASM, exclude cairo backend and font face manager (JS backend replaces them) + list(REMOVE_ITEM ${PROJECT_NAME}_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/cairo_render_backend.cpp) + list(REMOVE_ITEM ${PROJECT_NAME}_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/render_font_face_manager.cpp) endif() if (USE_FONT_MANAGER) @@ -38,16 +41,27 @@ if (RENDER_ENABLE_CJK) target_compile_definitions(${PROJECT_NAME} PRIVATE RENDER_ENABLE_CJK) endif() -target_link_libraries(${PROJECT_NAME} PUBLIC lunasvg) - -if (BUILD_STANDALONE) - target_link_libraries(${PROJECT_NAME} - PUBLIC rapidjson - PUBLIC cairo) -else () - find_package(Cairo REQUIRED) - find_package(RapidJSON REQUIRED) - target_link_libraries(${PROJECT_NAME} - PUBLIC RapidJSON::RapidJSON - PUBLIC Cairo::Cairo) -endif () +if (NOT EMSCRIPTEN) + # Desktop/server builds: link cairo, lunasvg, and their dependencies + target_link_libraries(${PROJECT_NAME} PUBLIC lunasvg) + + if (BUILD_STANDALONE) + target_link_libraries(${PROJECT_NAME} + PUBLIC rapidjson + PUBLIC cairo) + else () + find_package(Cairo REQUIRED) + find_package(RapidJSON REQUIRED) + target_link_libraries(${PROJECT_NAME} + PUBLIC RapidJSON::RapidJSON + PUBLIC Cairo::Cairo) + endif () +else() + # WASM builds: no graphics library dependencies, only rapidjson + if (BUILD_STANDALONE) + target_link_libraries(${PROJECT_NAME} PUBLIC rapidjson) + else() + find_package(RapidJSON REQUIRED) + target_link_libraries(${PROJECT_NAME} PUBLIC RapidJSON::RapidJSON) + endif() +endif() diff --git a/core/render2d/cairo_render_backend.h b/core/render2d/cairo_render_backend.h new file mode 100644 index 0000000000..edb4f18b23 --- /dev/null +++ b/core/render2d/cairo_render_backend.h @@ -0,0 +1,142 @@ +/**************************************************************************** + * Copyright (C) from 2009 to Present EPAM Systems. + * + * This file is part of Indigo toolkit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************/ + +#ifndef __cairo_render_backend_h__ +#define __cairo_render_backend_h__ + +#ifndef __EMSCRIPTEN__ + +#include "render_backend.h" + +#include +#include +#include +#include + +namespace indigo +{ + + class CairoRenderBackend : public IRenderBackend + { + public: + CairoRenderBackend(); + ~CairoRenderBackend() override; + + // ---- Surface lifecycle ---- + void createSurface(int mode, int width, int height, void* output) override; + void closeSurface(int mode, bool discard, void* output) override; + void createContext() override; + void destroyContext() override; + + // ---- Path operations ---- + void beginPath() override; + void closePath() override; + void moveTo(double x, double y) override; + void lineTo(double x, double y) override; + void relMoveTo(double dx, double dy) override; + void relLineTo(double dx, double dy) override; + void curveTo(double x1, double y1, double x2, double y2, double x3, double y3) override; + void arc(double cx, double cy, double r, double a0, double a1) override; + void arcNegative(double cx, double cy, double r, double a0, double a1) override; + void rect(double x, double y, double w, double h) override; + + // ---- Drawing operations ---- + void fill() override; + void stroke() override; + void paint() override; + + // ---- Style ---- + void setSourceRGB(double r, double g, double b) override; + void setSourceRGBA(double r, double g, double b, double a) override; + void setLineWidth(double w) override; + void setLineJoin(int join) override; + void setDash(const double* pattern, int count, double offset) override; + void setOperator(int op) override; + void setAntialias(int mode) override; + + // ---- Transform ---- + void save() override; + void restore() override; + void translate(double dx, double dy) override; + void scale(double sx, double sy) override; + void rotate(double angle) override; + void getMatrix(double m[6]) override; + void setMatrix(const double m[6]) override; + void initIdentityMatrix(double m[6]) override; + void userToDevice(double& x, double& y) override; + void deviceToUser(double& x, double& y) override; + + // ---- Extents ---- + void strokeExtents(double& x1, double& y1, double& x2, double& y2) override; + void pathExtents(double& x1, double& y1, double& x2, double& y2) override; + + // ---- Text ---- + void selectFontFace(const char* family, bool italic, bool bold) override; + void setFontSize(double size) override; + void textExtents(const char* text, double& width, double& height, double& x_bearing, double& y_bearing) override; + void fontExtents(double& height) override; + void showText(const char* text) override; + void textPath(const char* text) override; + + // ---- Font options ---- + void createFontOptions() override; + void destroyFontOptions() override; + void setFontOptionsAntialias(int mode) override; + void applyFontOptions() override; + + // ---- Gradient ---- + void setLinearGradient(double x0, double y0, double x1, double y1, double r1, double g1, double b1, double r2, double g2, double b2) override; + void clearPattern() override; + + // ---- Image ---- + void drawPngImage(const void* data, int dataLen, double x, double y, double w, double h) override; + void writeSurfaceToPng(void* output) override; + + // ---- Path debugging ---- + bool isPathEmpty() override; + + // ---- Surface source ---- + void setSourceSurface(double x, double y) override; + + // Access raw cairo objects (needed during transition) + cairo_t* getCr() + { + return _cr; + } + cairo_surface_t* getSurface() + { + return _surface; + } + + private: + static cairo_status_t _writer(void* closure, const unsigned char* data, unsigned int length); + + cairo_t* _cr; + cairo_surface_t* _surface; + cairo_surface_t* _pngImage; // temp for drawPngImage + cairo_pattern_t* _pattern; + cairo_font_options_t* _fontOptions; + + static std::mutex _mutex; + }; + +} // namespace indigo + +#endif // !__EMSCRIPTEN__ + +#endif diff --git a/core/render2d/font_lang_detector.h b/core/render2d/font_lang_detector.h new file mode 100644 index 0000000000..9cde4ac396 --- /dev/null +++ b/core/render2d/font_lang_detector.h @@ -0,0 +1,222 @@ +/**************************************************************************** + * Copyright (C) from 2009 to Present EPAM Systems. + * + * This file is part of Indigo toolkit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************/ + +#pragma once + +#include "render_common.h" + +#include +#include + +namespace indigo +{ + + class CharacterRange + { + private: + int _first; + int _last; + + public: + bool isInRange(int id) const + { + return id >= _first && id <= _last; + } + CharacterRange(int first, int last) : _first(first), _last(last) + { + } + ~CharacterRange() + { + } + }; + + class FontRanges + { + private: + std::vector _ranges; + + public: + void addRange(const CharacterRange& range) + { + _ranges.push_back(range); + } + + bool isInRanges(int idx) + { + for (const auto& r : _ranges) + { + if (r.isInRange(idx)) + { + return true; + } + } + return false; + } + + FontRanges() + { + } + ~FontRanges() + { + } + }; + + enum FONT_LANG + { + NO_CJK, + CJK, + KOREAN, + JAPANESE + }; + + class FontLangDetector + { + private: + FontRanges _j_ranges; // japan + FontRanges _k_ranges; // korean + FontRanges _cjk_common_ranges; // common_cjk_ranges + + void prepareRanges() + { + CharacterRange CJK_Unified_Ideographs(0x4E00, 0x9FFF); // 一丁 + CharacterRange CJK_Compatibility_Ideographs(0xF900, 0xFAFF); // 豈類 + CharacterRange CJK_Unified_Ideographs_Extension_A(0x3400, 0x4DBF); // 㐀㐁 + + _cjk_common_ranges.addRange(CJK_Unified_Ideographs); + _cjk_common_ranges.addRange(CJK_Compatibility_Ideographs); + _cjk_common_ranges.addRange(CJK_Unified_Ideographs_Extension_A); + + // Korean + // https://en.wikipedia.org/wiki/Hangul + // U+AC00-U+D7AF Hangul Syllables + // U+1100–U+11FF Hangul Jamo + // U+3130–U+318F Hangul Compatibility Jamo + // U+A960–U+A97F Hangul Jamo Extended-A + // U+D7B0–U+D7FF Hangul Jamo Extended-B + // TODO: Hangul subset of Enclosed CJK Letters and Months + // TODO: Hangul subset of Halfwidth and Fullwidth Forms + CharacterRange Hangul_Syllables(0xAC00, 0xD7AF); // 가각 + CharacterRange Hangul_Jamo(0x1100, 0x11FF); // ᄀᄁ + CharacterRange Hangul_Compatibility_Jamo(0x3130, 0x318F); // ㄱㄲ + CharacterRange Hangul_Jamo_Extended_A(0xA960, 0xA97F); // ꥠꥡ + CharacterRange Hangul_Jamo_Extended_B(0xD7B0, 0xD7FF); // ힰힱ + + _k_ranges.addRange(Hangul_Syllables); + _k_ranges.addRange(Hangul_Jamo); + _k_ranges.addRange(Hangul_Compatibility_Jamo); + + // Japan + // https://en.wikipedia.org/wiki/Japanese_writing_system + // U+4E00–U+9FBF Kanji - same range as CJK_Unified_Ideographs + // U+3040–U+309F Hiragana + // U+30A0–U+30FF Katakana + CharacterRange Hiragana(0x3040, 0x309F); // ぁあ + CharacterRange Katakana(0x30A0, 0x30FF); // ゠ァジ + + _j_ranges.addRange(Hiragana); + _j_ranges.addRange(Katakana); + } + + // TODO: use wstring_convert instead of custom converter + std::vector utf8_indexes(const TextItem& ti) + { + std::vector indexes; + for (int i = 0; i < ti.text.size();) + { + unsigned int utf8_char = static_cast(ti.text[i]); + ++i; + if (utf8_char >= 0x80) + { + int extra_bytes = 0; + if ((utf8_char & 0xE0) == 0xC0) + { + extra_bytes = 1; + utf8_char &= 0x1F; + } + else if ((utf8_char & 0xF0) == 0xE0) + { + extra_bytes = 2; + utf8_char &= 0x0F; + } + else if ((utf8_char & 0xF8) == 0xF0) + { + extra_bytes = 3; + utf8_char &= 0x07; + } + while (extra_bytes && i < ti.text.size()) + { + unsigned char cont_byte = static_cast(ti.text[i]); + if ((cont_byte & 0xC0) != 0x80) + { + break; + } + utf8_char = (utf8_char << 6) | (cont_byte & 0x3F); + ++i; + --extra_bytes; + } + } + indexes.push_back(utf8_char); + } + return indexes; + } + + public: + FontLangDetector() + { + prepareRanges(); + }; + ~FontLangDetector(){}; + + FONT_LANG detectLang(const TextItem& ti) + { + if (ti.text.size() == 0) + { + return FONT_LANG::NO_CJK; + } + + std::vector indexes = utf8_indexes(ti); + + for (auto idx : indexes) + { + if (_k_ranges.isInRanges(idx)) + { + return FONT_LANG::KOREAN; + } + } + + for (auto idx : indexes) + { + if (_j_ranges.isInRanges(idx)) + { + return FONT_LANG::JAPANESE; + } + } + + for (auto idx : indexes) + { + if (_cjk_common_ranges.isInRanges(idx)) + { + return FONT_LANG::CJK; + } + } + + return FONT_LANG::NO_CJK; + } + }; + +} // namespace indigo diff --git a/core/render2d/js_render_backend.h b/core/render2d/js_render_backend.h new file mode 100644 index 0000000000..0216b12225 --- /dev/null +++ b/core/render2d/js_render_backend.h @@ -0,0 +1,135 @@ +/**************************************************************************** + * Copyright (C) from 2009 to Present EPAM Systems. + * + * This file is part of Indigo toolkit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************/ + +#ifndef __js_render_backend_h__ +#define __js_render_backend_h__ + +#ifdef __EMSCRIPTEN__ + +#include "render_backend.h" + +namespace indigo +{ + + // Rendering backend that delegates all drawing to JavaScript Canvas2D API via EM_JS calls. + // Used in WASM builds to avoid shipping cairo, freetype, libpng, lunasvg, plutovg, pixman. + class JSRenderBackend : public IRenderBackend + { + public: + JSRenderBackend(); + ~JSRenderBackend() override; + + // ---- Surface lifecycle ---- + void createSurface(int mode, int width, int height, void* output) override; + void closeSurface(int mode, bool discard, void* output) override; + void createContext() override; + void destroyContext() override; + + // ---- Path operations ---- + void beginPath() override; + void closePath() override; + void moveTo(double x, double y) override; + void lineTo(double x, double y) override; + void relMoveTo(double dx, double dy) override; + void relLineTo(double dx, double dy) override; + void curveTo(double x1, double y1, double x2, double y2, double x3, double y3) override; + void arc(double cx, double cy, double r, double a0, double a1) override; + void arcNegative(double cx, double cy, double r, double a0, double a1) override; + void rect(double x, double y, double w, double h) override; + + // ---- Drawing operations ---- + void fill() override; + void stroke() override; + void paint() override; + + // ---- Style ---- + void setSourceRGB(double r, double g, double b) override; + void setSourceRGBA(double r, double g, double b, double a) override; + void setLineWidth(double w) override; + void setLineJoin(int join) override; + void setDash(const double* pattern, int count, double offset) override; + void setOperator(int op) override; + void setAntialias(int mode) override; + + // ---- Transform ---- + void save() override; + void restore() override; + void translate(double dx, double dy) override; + void scale(double sx, double sy) override; + void rotate(double angle) override; + void getMatrix(double m[6]) override; + void setMatrix(const double m[6]) override; + void initIdentityMatrix(double m[6]) override; + void userToDevice(double& x, double& y) override; + void deviceToUser(double& x, double& y) override; + + // ---- Extents ---- + void strokeExtents(double& x1, double& y1, double& x2, double& y2) override; + void pathExtents(double& x1, double& y1, double& x2, double& y2) override; + + // ---- Text ---- + void selectFontFace(const char* family, bool italic, bool bold) override; + void setFontSize(double size) override; + void textExtents(const char* text, double& width, double& height, double& x_bearing, double& y_bearing) override; + void fontExtents(double& height) override; + void showText(const char* text) override; + void textPath(const char* text) override; + + // ---- Font options ---- + void createFontOptions() override; + void destroyFontOptions() override; + void setFontOptionsAntialias(int mode) override; + void applyFontOptions() override; + + // ---- Gradient ---- + void setLinearGradient(double x0, double y0, double x1, double y1, double r1, double g1, double b1, double r2, double g2, double b2) override; + void clearPattern() override; + + // ---- Image ---- + void drawPngImage(const void* data, int dataLen, double x, double y, double w, double h) override; + void writeSurfaceToPng(void* output) override; + + // ---- Path debugging ---- + bool isPathEmpty() override; + + // ---- Surface source ---- + void setSourceSurface(double x, double y) override; + + private: + int _width; + int _height; + int _mode; + + // Current transform matrix (tracked in C++ for userToDevice/deviceToUser) + double _matrix[6]; // [xx, yx, xy, yy, x0, y0] + + // Font state + double _fontSize; + bool _fontBold; + bool _fontItalic; + char _fontFamily[256]; + + // Current position (for relMoveTo/relLineTo) + double _curX, _curY; + }; + +} // namespace indigo + +#endif // __EMSCRIPTEN__ + +#endif diff --git a/core/render2d/render_backend.h b/core/render2d/render_backend.h new file mode 100644 index 0000000000..3661ff003c --- /dev/null +++ b/core/render2d/render_backend.h @@ -0,0 +1,135 @@ +/**************************************************************************** + * Copyright (C) from 2009 to Present EPAM Systems. + * + * This file is part of Indigo toolkit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************/ + +#ifndef __render_backend_h__ +#define __render_backend_h__ + +#include + +namespace indigo +{ + + // Output format modes (mirrors existing MODE_* constants) + enum RenderBackendMode + { + RBMODE_PNG, + RBMODE_SVG, + RBMODE_PDF, + RBMODE_HDC, + RBMODE_PRN, + RBMODE_EMF + }; + + // Abstract rendering backend interface. + // Decouples render2d drawing logic from a specific graphics library. + // Desktop builds use CairoRenderBackend; WASM builds use JSRenderBackend. + class IRenderBackend + { + public: + virtual ~IRenderBackend() = default; + + // ---- Surface lifecycle ---- + // Create the drawing surface. `output` is an opaque pointer to an Output* (used by cairo backend). + virtual void createSurface(int mode, int width, int height, void* output) = 0; + // Finalize the surface and write to output. For JS backend, may produce base64 PNG or SVG string. + virtual void closeSurface(int mode, bool discard, void* output) = 0; + // Create cairo context (or JS canvas context) + virtual void createContext() = 0; + // Destroy the context + virtual void destroyContext() = 0; + + // ---- Path operations ---- + virtual void beginPath() = 0; + virtual void closePath() = 0; + virtual void moveTo(double x, double y) = 0; + virtual void lineTo(double x, double y) = 0; + virtual void relMoveTo(double dx, double dy) = 0; + virtual void relLineTo(double dx, double dy) = 0; + virtual void curveTo(double x1, double y1, double x2, double y2, double x3, double y3) = 0; + virtual void arc(double cx, double cy, double r, double a0, double a1) = 0; + virtual void arcNegative(double cx, double cy, double r, double a0, double a1) = 0; + virtual void rect(double x, double y, double w, double h) = 0; + + // ---- Drawing operations ---- + virtual void fill() = 0; + virtual void stroke() = 0; + virtual void paint() = 0; + + // ---- Style ---- + virtual void setSourceRGB(double r, double g, double b) = 0; + virtual void setSourceRGBA(double r, double g, double b, double a) = 0; + virtual void setLineWidth(double w) = 0; + virtual void setLineJoin(int join) = 0; // 0=miter, 1=round, 2=bevel + virtual void setDash(const double* pattern, int count, double offset) = 0; + virtual void setOperator(int op) = 0; // CAIRO_OPERATOR_SOURCE = 1 + virtual void setAntialias(int mode) = 0; + + // ---- Transform ---- + virtual void save() = 0; + virtual void restore() = 0; + virtual void translate(double dx, double dy) = 0; + virtual void scale(double sx, double sy) = 0; + virtual void rotate(double angle) = 0; + + // Matrix operations: get/set current transform matrix as 6 doubles [xx, yx, xy, yy, x0, y0] + virtual void getMatrix(double m[6]) = 0; + virtual void setMatrix(const double m[6]) = 0; + virtual void initIdentityMatrix(double m[6]) = 0; + + // Coordinate transform + virtual void userToDevice(double& x, double& y) = 0; + virtual void deviceToUser(double& x, double& y) = 0; + + // ---- Extents ---- + virtual void strokeExtents(double& x1, double& y1, double& x2, double& y2) = 0; + virtual void pathExtents(double& x1, double& y1, double& x2, double& y2) = 0; + + // ---- Text ---- + virtual void selectFontFace(const char* family, bool italic, bool bold) = 0; + virtual void setFontSize(double size) = 0; + virtual void textExtents(const char* text, double& width, double& height, double& x_bearing, double& y_bearing) = 0; + virtual void fontExtents(double& height) = 0; + virtual void showText(const char* text) = 0; + virtual void textPath(const char* text) = 0; + + // ---- Font options ---- + virtual void createFontOptions() = 0; + virtual void destroyFontOptions() = 0; + virtual void setFontOptionsAntialias(int mode) = 0; + virtual void applyFontOptions() = 0; + + // ---- Gradient ---- + virtual void setLinearGradient(double x0, double y0, double x1, double y1, double r1, double g1, double b1, double r2, double g2, double b2) = 0; + virtual void clearPattern() = 0; + + // ---- Image operations ---- + // Draw a PNG image from raw data into the given bounding box + virtual void drawPngImage(const void* data, int dataLen, double x, double y, double w, double h) = 0; + // Write surface to PNG stream + virtual void writeSurfaceToPng(void* output) = 0; + + // ---- Path debugging ---- + virtual bool isPathEmpty() = 0; + + // ---- Surface source ---- + virtual void setSourceSurface(double x, double y) = 0; + }; + +} // namespace indigo + +#endif diff --git a/core/render2d/render_context.h b/core/render2d/render_context.h index 3e93aba952..445bd360bf 100644 --- a/core/render2d/render_context.h +++ b/core/render2d/render_context.h @@ -19,11 +19,16 @@ #ifndef __render_context_h__ #define __render_context_h__ +#ifndef __EMSCRIPTEN__ #include #include #include +#endif + +#include #include +#include "render_backend.h" #include "render_common.h" #ifdef RENDER_USE_FONT_MANAGER @@ -33,6 +38,21 @@ namespace indigo { + // 6-element transform matrix [xx, yx, xy, yy, x0, y0] — replaces cairo_matrix_t + struct RenderMatrix + { + double m[6]; + RenderMatrix() + { + m[0] = 1; + m[1] = 0; + m[2] = 0; + m[3] = 1; + m[4] = 0; + m[5] = 0; + } + }; + class RenderContext { public: @@ -53,7 +73,7 @@ namespace indigo void setLineWidth(double width); void setFontFamily(const char* ff); void setOutput(Output* output); - void createSurface(cairo_write_func_t writer, Output* output, int width, int height); + void createSurface(int width, int height); void init(); void fillBackground(); void initNullContext(); @@ -153,13 +173,13 @@ namespace indigo return _height; } - void cairoCheckStatus() const; - void cairoCheckSurfaceStatus() const; + void backendCheckStatus() const; #ifdef _WIN32 - cairo_surface_t* createWin32Surface(); - cairo_surface_t* createWin32PrintingSurfaceForHDC(); - cairo_surface_t* createWin32PrintingSurfaceForMetafile(bool& isLarge); + // Win32-specific rendering (requires cairo) + void* createWin32Surface(); + void* createWin32PrintingSurfaceForHDC(); + void* createWin32PrintingSurfaceForMetafile(bool& isLarge); void storeAndDestroyMetafile(bool discard); #endif @@ -168,7 +188,7 @@ namespace indigo void fontsDispose(); double fontGetSize(FONT_SIZE size); void fontsSetFont(const TextItem& ti); - void fontsGetTextExtents(cairo_t* cr, const char* text, int size, float& dx, float& dy, float& rx, float& ry); + void fontsGetTextExtents(const char* text, int size, float& dx, float& dy, float& rx, float& ry); float getSpaceWidth(); void fontsDrawText(const TextItem& ti, const Vec3f& color, bool idle); @@ -183,8 +203,6 @@ namespace indigo Vec2f bbmin, bbmax; private: - static cairo_status_t writer(void* closure, const unsigned char* data, unsigned int length); - void _drawGraphItem(GraphItem& gi); void lineTo(const Vec2f& v); void lineToRel(float x, float y); @@ -192,7 +210,7 @@ namespace indigo void moveTo(const Vec2f& v); void moveToRel(float x, float y); void moveToRel(const Vec2f& v); - void arc(cairo_t* cr, double xc, double yc, double radius, double angle1, double angle2); + void _arc(double xc, double yc, double radius, double angle1, double angle2); int _width; int _height; @@ -200,25 +218,19 @@ namespace indigo Vec3f _backColor; Vec3f _baseColor; float _currentLineWidth; - cairo_pattern_t* _pattern; - static std::mutex _cairo_mutex; + std::unique_ptr _backend; + + static std::mutex _mutex; CP_DECL; TL_CP_DECL(Array, _fontfamily); - TL_CP_DECL(Array, transforms); + TL_CP_DECL(Array, transforms); #ifdef _WIN32 void* _h_fonts[FONT_SIZE_COUNT * 2]; #endif - cairo_font_face_t *cairoFontFaceRegular, *cairoFontFaceBold; - cairo_matrix_t fontScale, fontCtm; - cairo_font_options_t* fontOptions; - cairo_scaled_font_t* _scaled_fonts[FONT_SIZE_COUNT * 2]; - bool metafileFontsToCurves; - cairo_t* _cr; - cairo_surface_t* _surface; void* _meta_hdc; #ifdef RENDER_USE_FONT_MANAGER diff --git a/core/render2d/render_font_face_manager.h b/core/render2d/render_font_face_manager.h index 1f4e7eafb9..7011c8f078 100644 --- a/core/render2d/render_font_face_manager.h +++ b/core/render2d/render_font_face_manager.h @@ -18,6 +18,7 @@ #pragma once +#include "font_lang_detector.h" #include "render_common.h" #include @@ -27,202 +28,6 @@ #include namespace indigo { - -#ifdef RENDER_ENABLE_CJK - class CharacterRange - { - private: - int _first; - int _last; - - public: - bool isInRange(int id) const - { - return id >= _first && id <= _last; - } - CharacterRange(int first, int last) : _first(first), _last(last) - { - } - ~CharacterRange() - { - } - }; - - class FontRanges - { - private: - std::vector _ranges; - - public: - void addRange(const CharacterRange& range) - { - _ranges.push_back(range); - } - - bool isInRanges(int idx) - { - for (const auto& r : _ranges) - { - if (r.isInRange(idx)) - { - return true; - } - } - return false; - } - - FontRanges() - { - } - ~FontRanges() - { - } - }; - - enum FONT_LANG - { - NO_CJK, - CJK, - KOREAN, - JAPANESE - }; - - class FontLangDetector - { - private: - FontRanges _j_ranges; // japan - FontRanges _k_ranges; // korean - FontRanges _cjk_common_ranges; // common_cjk_ranges - - void prepareRanges() - { - CharacterRange CJK_Unified_Ideographs(0x4E00, 0x9FFF); // 一丁 - CharacterRange CJK_Compatibility_Ideographs(0xF900, 0xFAFF); // 豈類 - CharacterRange CJK_Unified_Ideographs_Extension_A(0x3400, 0x4DBF); // 㐀㐁 - - _cjk_common_ranges.addRange(CJK_Unified_Ideographs); - _cjk_common_ranges.addRange(CJK_Compatibility_Ideographs); - _cjk_common_ranges.addRange(CJK_Unified_Ideographs_Extension_A); - - // Korean - // https://en.wikipedia.org/wiki/Hangul - // U+AC00-U+D7AF Hangul Syllables - // U+1100–U+11FF Hangul Jamo - // U+3130–U+318F Hangul Compatibility Jamo - // U+A960–U+A97F Hangul Jamo Extended-A - // U+D7B0–U+D7FF Hangul Jamo Extended-B - // TODO: Hangul subset of Enclosed CJK Letters and Months - // TODO: Hangul subset of Halfwidth and Fullwidth Forms - CharacterRange Hangul_Syllables(0xAC00, 0xD7AF); // 가각 - CharacterRange Hangul_Jamo(0x1100, 0x11FF); // ᄀᄁ - CharacterRange Hangul_Compatibility_Jamo(0x3130, 0x318F); // ㄱㄲ - CharacterRange Hangul_Jamo_Extended_A(0xA960, 0xA97F); // ꥠꥡ - CharacterRange Hangul_Jamo_Extended_B(0xD7B0, 0xD7FF); // ힰힱ - - _k_ranges.addRange(Hangul_Syllables); - _k_ranges.addRange(Hangul_Jamo); - _k_ranges.addRange(Hangul_Compatibility_Jamo); - - // Japan - // https://en.wikipedia.org/wiki/Japanese_writing_system - // U+4E00–U+9FBF Kanji - same range as CJK_Unified_Ideographs - // U+3040–U+309F Hiragana - // U+30A0–U+30FF Katakana - CharacterRange Hiragana(0x3040, 0x309F); // ぁあ - CharacterRange Katakana(0x30A0, 0x30FF); // ゠ァジ - - _j_ranges.addRange(Hiragana); - _j_ranges.addRange(Katakana); - } - - // TODO: use wstring_convert instead of custom converter - std::vector utf8_indexes(const TextItem& ti) - { - std::vector indexes; - for (unsigned int i = 0; i < ti.text.size();) - { - unsigned int utf8_char = static_cast(ti.text[i]); - ++i; - if (utf8_char >= 0x80) - { - int extra_bytes = 0; - if ((utf8_char & 0xE0) == 0xC0) - { - extra_bytes = 1; - utf8_char &= 0x1F; - } - else if ((utf8_char & 0xF0) == 0xE0) - { - extra_bytes = 2; - utf8_char &= 0x0F; - } - else if ((utf8_char & 0xF8) == 0xF0) - { - extra_bytes = 3; - utf8_char &= 0x07; - } - while (extra_bytes && i < ti.text.size()) - { - unsigned char cont_byte = static_cast(ti.text[i]); - if ((cont_byte & 0xC0) != 0x80) - { - break; - } - utf8_char = (utf8_char << 6) | (cont_byte & 0x3F); - ++i; - --extra_bytes; - } - } - indexes.push_back(utf8_char); - } - return indexes; - } - - public: - FontLangDetector() - { - prepareRanges(); - }; - ~FontLangDetector(){}; - - FONT_LANG detectLang(const TextItem& ti) - { - if (ti.text.size() == 0) - { - return FONT_LANG::NO_CJK; - } - - std::vector indexes = utf8_indexes(ti); - - for (auto idx : indexes) - { - if (_k_ranges.isInRanges(idx)) - { - return FONT_LANG::KOREAN; - } - } - - for (auto idx : indexes) - { - if (_j_ranges.isInRanges(idx)) - { - return FONT_LANG::JAPANESE; - } - } - - for (auto idx : indexes) - { - if (_cjk_common_ranges.isInRanges(idx)) - { - return FONT_LANG::CJK; - } - } - - return FONT_LANG::NO_CJK; - } - }; -#endif - class RenderFontFaceManager { struct Face diff --git a/core/render2d/src/cairo_render_backend.cpp b/core/render2d/src/cairo_render_backend.cpp new file mode 100644 index 0000000000..47939eed9b --- /dev/null +++ b/core/render2d/src/cairo_render_backend.cpp @@ -0,0 +1,475 @@ +/**************************************************************************** + * Copyright (C) from 2009 to Present EPAM Systems. + * + * This file is part of Indigo toolkit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************/ + +#ifndef __EMSCRIPTEN__ + +#include "cairo_render_backend.h" +#include "base_cpp/output.h" + +#include + +using namespace indigo; + +std::mutex CairoRenderBackend::_mutex; + +CairoRenderBackend::CairoRenderBackend() : _cr(nullptr), _surface(nullptr), _pngImage(nullptr), _pattern(nullptr), _fontOptions(nullptr) +{ +} + +CairoRenderBackend::~CairoRenderBackend() +{ + if (_pattern) + cairo_pattern_destroy(_pattern); + if (_fontOptions) + cairo_font_options_destroy(_fontOptions); + if (_cr) + cairo_destroy(_cr); + if (_surface) + cairo_surface_destroy(_surface); +} + +// ---- writer callback ---- + +cairo_status_t CairoRenderBackend::_writer(void* closure, const unsigned char* data, unsigned int length) +{ + try + { + ((Output*)closure)->write(data, length); + } + catch (Output::Error&) + { + return CAIRO_STATUS_WRITE_ERROR; + } + return CAIRO_STATUS_SUCCESS; +} + +// ---- Surface lifecycle ---- + +void CairoRenderBackend::createSurface(int mode, int width, int height, void* output) +{ + std::lock_guard lock(_mutex); + switch (mode) + { + case RBMODE_PDF: + _surface = cairo_pdf_surface_create_for_stream(_writer, output, width, height); + break; + case RBMODE_SVG: + _surface = cairo_svg_surface_create_for_stream(_writer, output, width, height); + break; + case RBMODE_PNG: + _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, width, height); + break; + default: + break; // HDC/PRN/EMF handled separately in RenderContext + } +} + +void CairoRenderBackend::closeSurface(int mode, bool discard, void* output) +{ + if (mode == RBMODE_PNG && !discard && _surface) + { + cairo_surface_write_to_png_stream(_surface, _writer, output); + } + + if (_surface) + { + std::lock_guard lock(_mutex); + cairo_surface_destroy(_surface); + _surface = nullptr; + } +} + +void CairoRenderBackend::createContext() +{ + _cr = cairo_create(_surface); +} + +void CairoRenderBackend::destroyContext() +{ + if (_cr) + { + std::lock_guard lock(_mutex); + cairo_destroy(_cr); + _cr = nullptr; + } +} + +// ---- Path operations ---- + +void CairoRenderBackend::beginPath() +{ + cairo_new_path(_cr); +} + +void CairoRenderBackend::closePath() +{ + cairo_close_path(_cr); +} + +void CairoRenderBackend::moveTo(double x, double y) +{ + cairo_move_to(_cr, x, y); +} + +void CairoRenderBackend::lineTo(double x, double y) +{ + cairo_line_to(_cr, x, y); +} + +void CairoRenderBackend::relMoveTo(double dx, double dy) +{ + cairo_rel_move_to(_cr, dx, dy); +} + +void CairoRenderBackend::relLineTo(double dx, double dy) +{ + cairo_rel_line_to(_cr, dx, dy); +} + +void CairoRenderBackend::curveTo(double x1, double y1, double x2, double y2, double x3, double y3) +{ + cairo_curve_to(_cr, x1, y1, x2, y2, x3, y3); +} + +void CairoRenderBackend::arc(double cx, double cy, double r, double a0, double a1) +{ + cairo_arc(_cr, cx, cy, r, a0, a1); +} + +void CairoRenderBackend::arcNegative(double cx, double cy, double r, double a0, double a1) +{ + cairo_arc_negative(_cr, cx, cy, r, a0, a1); +} + +void CairoRenderBackend::rect(double x, double y, double w, double h) +{ + cairo_rectangle(_cr, x, y, w, h); +} + +// ---- Drawing operations ---- + +void CairoRenderBackend::fill() +{ + cairo_fill(_cr); +} + +void CairoRenderBackend::stroke() +{ + cairo_stroke(_cr); +} + +void CairoRenderBackend::paint() +{ + cairo_paint(_cr); +} + +// ---- Style ---- + +void CairoRenderBackend::setSourceRGB(double r, double g, double b) +{ + cairo_set_source_rgb(_cr, r, g, b); +} + +void CairoRenderBackend::setSourceRGBA(double r, double g, double b, double a) +{ + cairo_set_source_rgba(_cr, r, g, b, a); +} + +void CairoRenderBackend::setLineWidth(double w) +{ + cairo_set_line_width(_cr, w); +} + +void CairoRenderBackend::setLineJoin(int join) +{ + cairo_line_join_t j; + switch (join) + { + case 0: + j = CAIRO_LINE_JOIN_MITER; + break; + case 1: + j = CAIRO_LINE_JOIN_ROUND; + break; + default: + j = CAIRO_LINE_JOIN_BEVEL; + break; + } + cairo_set_line_join(_cr, j); +} + +void CairoRenderBackend::setDash(const double* pattern, int count, double offset) +{ + cairo_set_dash(_cr, pattern, count, offset); +} + +void CairoRenderBackend::setOperator(int op) +{ + cairo_set_operator(_cr, (cairo_operator_t)op); +} + +void CairoRenderBackend::setAntialias(int mode) +{ + cairo_set_antialias(_cr, (cairo_antialias_t)mode); +} + +// ---- Transform ---- + +void CairoRenderBackend::save() +{ + cairo_save(_cr); +} + +void CairoRenderBackend::restore() +{ + cairo_restore(_cr); +} + +void CairoRenderBackend::translate(double dx, double dy) +{ + cairo_translate(_cr, dx, dy); +} + +void CairoRenderBackend::scale(double sx, double sy) +{ + cairo_scale(_cr, sx, sy); +} + +void CairoRenderBackend::rotate(double angle) +{ + cairo_rotate(_cr, angle); +} + +void CairoRenderBackend::getMatrix(double m[6]) +{ + cairo_matrix_t mt; + cairo_get_matrix(_cr, &mt); + m[0] = mt.xx; + m[1] = mt.yx; + m[2] = mt.xy; + m[3] = mt.yy; + m[4] = mt.x0; + m[5] = mt.y0; +} + +void CairoRenderBackend::setMatrix(const double m[6]) +{ + cairo_matrix_t mt; + mt.xx = m[0]; + mt.yx = m[1]; + mt.xy = m[2]; + mt.yy = m[3]; + mt.x0 = m[4]; + mt.y0 = m[5]; + cairo_set_matrix(_cr, &mt); +} + +void CairoRenderBackend::initIdentityMatrix(double m[6]) +{ + cairo_matrix_t mt; + cairo_matrix_init_identity(&mt); + m[0] = mt.xx; + m[1] = mt.yx; + m[2] = mt.xy; + m[3] = mt.yy; + m[4] = mt.x0; + m[5] = mt.y0; +} + +void CairoRenderBackend::userToDevice(double& x, double& y) +{ + cairo_user_to_device(_cr, &x, &y); +} + +void CairoRenderBackend::deviceToUser(double& x, double& y) +{ + cairo_device_to_user(_cr, &x, &y); +} + +// ---- Extents ---- + +void CairoRenderBackend::strokeExtents(double& x1, double& y1, double& x2, double& y2) +{ + cairo_stroke_extents(_cr, &x1, &y1, &x2, &y2); +} + +void CairoRenderBackend::pathExtents(double& x1, double& y1, double& x2, double& y2) +{ + cairo_path_extents(_cr, &x1, &y1, &x2, &y2); +} + +// ---- Text ---- + +void CairoRenderBackend::selectFontFace(const char* family, bool italic, bool bold) +{ + std::lock_guard lock(_mutex); + cairo_select_font_face(_cr, family, italic ? CAIRO_FONT_SLANT_ITALIC : CAIRO_FONT_SLANT_NORMAL, bold ? CAIRO_FONT_WEIGHT_BOLD : CAIRO_FONT_WEIGHT_NORMAL); +} + +void CairoRenderBackend::setFontSize(double size) +{ + cairo_set_font_size(_cr, size); +} + +void CairoRenderBackend::textExtents(const char* text, double& width, double& height, double& x_bearing, double& y_bearing) +{ + std::lock_guard lock(_mutex); + cairo_text_extents_t te; + cairo_text_extents(_cr, text, &te); + width = te.width; + height = te.height; + x_bearing = te.x_bearing; + y_bearing = te.y_bearing; +} + +void CairoRenderBackend::fontExtents(double& height) +{ + cairo_font_extents_t fe; + cairo_font_extents(_cr, &fe); + height = fe.height; +} + +void CairoRenderBackend::showText(const char* text) +{ + std::lock_guard lock(_mutex); + cairo_show_text(_cr, text); +} + +void CairoRenderBackend::textPath(const char* text) +{ + std::lock_guard lock(_mutex); + cairo_text_path(_cr, text); +} + +// ---- Font options ---- + +void CairoRenderBackend::createFontOptions() +{ + _fontOptions = cairo_font_options_create(); +} + +void CairoRenderBackend::destroyFontOptions() +{ + if (_fontOptions) + { + cairo_font_options_destroy(_fontOptions); + _fontOptions = nullptr; + } +} + +void CairoRenderBackend::setFontOptionsAntialias(int mode) +{ + if (_fontOptions) + cairo_font_options_set_antialias(_fontOptions, (cairo_antialias_t)mode); +} + +void CairoRenderBackend::applyFontOptions() +{ + if (_fontOptions && _cr) + cairo_set_font_options(_cr, _fontOptions); +} + +// ---- Gradient ---- + +void CairoRenderBackend::setLinearGradient(double x0, double y0, double x1, double y1, double r1, double g1, double b1, double r2, double g2, double b2) +{ + if (_pattern) + { + cairo_pattern_destroy(_pattern); + _pattern = nullptr; + } + _pattern = cairo_pattern_create_linear(x0, y0, x1, y1); + cairo_pattern_add_color_stop_rgb(_pattern, 0, r1, g1, b1); + cairo_pattern_add_color_stop_rgb(_pattern, 1, r2, g2, b2); + cairo_set_source(_cr, _pattern); +} + +void CairoRenderBackend::clearPattern() +{ + if (_pattern) + { + cairo_pattern_destroy(_pattern); + _pattern = nullptr; + } +} + +// ---- Image ---- + +struct PngReadCtx +{ + const unsigned char* data; + size_t size; + size_t offset; +}; + +static cairo_status_t _pngReadFunc(void* closure, unsigned char* data, unsigned int length) +{ + PngReadCtx* ctx = static_cast(closure); + if (ctx->offset + length > ctx->size) + return CAIRO_STATUS_READ_ERROR; + memcpy(data, ctx->data + ctx->offset, length); + ctx->offset += length; + return CAIRO_STATUS_SUCCESS; +} + +void CairoRenderBackend::drawPngImage(const void* data, int dataLen, double x, double y, double w, double h) +{ + PngReadCtx ctx = {(const unsigned char*)data, (size_t)dataLen, 0}; + cairo_surface_t* image = cairo_image_surface_create_from_png_stream(_pngReadFunc, &ctx); + if (cairo_surface_status(image) != CAIRO_STATUS_SUCCESS) + { + cairo_surface_destroy(image); + return; + } + double imgW = cairo_image_surface_get_width(image); + double imgH = cairo_image_surface_get_height(image); + + cairo_save(_cr); + cairo_translate(_cr, x, y); + cairo_scale(_cr, w / imgW, h / imgH); + cairo_set_source_surface(_cr, image, 0, 0); + cairo_paint(_cr); + cairo_restore(_cr); + cairo_surface_destroy(image); +} + +void CairoRenderBackend::writeSurfaceToPng(void* output) +{ + if (_surface) + cairo_surface_write_to_png_stream(_surface, _writer, output); +} + +// ---- Path debugging ---- + +bool CairoRenderBackend::isPathEmpty() +{ + cairo_path_t* p = cairo_copy_path(_cr); + bool empty = (p->num_data == 0); + cairo_path_destroy(p); + return empty; +} + +// ---- Surface source ---- + +void CairoRenderBackend::setSourceSurface(double x, double y) +{ + if (_pngImage) + cairo_set_source_surface(_cr, _pngImage, x, y); +} + +#endif // !__EMSCRIPTEN__ diff --git a/core/render2d/src/js_render_backend.cpp b/core/render2d/src/js_render_backend.cpp new file mode 100644 index 0000000000..fde8a3cb19 --- /dev/null +++ b/core/render2d/src/js_render_backend.cpp @@ -0,0 +1,1115 @@ +/**************************************************************************** + * Copyright (C) from 2009 to Present EPAM Systems. + * + * This file is part of Indigo toolkit. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + ***************************************************************************/ + +#ifdef __EMSCRIPTEN__ + +#include "js_render_backend.h" +#include "base_cpp/output.h" +#include +#include +#include +#include +#include + +using namespace indigo; + +// ============================================================================ +// EM_JS: SVG builder - all drawing operations build SVG XML in JavaScript +// =================================================== + +EM_JS(void, js_rb_init, (int w, int h, int mode), { + Module._rb = { + w : w, + h : h, + mode : mode, + // State + s : { + ctm : [ 1, 0, 0, 1, 0, 0 ], + r : 0, + g : 0, + b : 0, + a : 1, + lw : 1, + lj : 'miter', + dash : [], + doff : 0, + fsz : 12, + ffam : 'Noto Sans', + fbold : false, + fital : false + }, + stack : [], + // Path + pd : '', + cx : 0, + cy : 0, + pathEmpty : true, + // Bounding box tracking (in final pixel coords) + bbx0 : 1e9, + bby0 : 1e9, + bbx1 : -1e9, + bby1 : -1e9, + // Per-path bbox (reset on each emitPath) + pbbx0 : 1e9, + pbby0 : 1e9, + pbbx1 : -1e9, + pbby1 : -1e9, + // SVG output + elems : [], + defs : '', + gradCnt : 0, + gradId : null, + out : null + }; +}); + +EM_JS(void, js_rb_destroy, (), { Module._rb = null; }); + +// -- Path operations -- +EM_JS(void, js_rb_beginPath, (), { + var r = Module._rb; + r.pd =''; + r.pathEmpty = true; +}); +EM_JS(void, js_rb_closePath, (), { Module._rb.pd += 'Z '; }); + +// Helper: transform point through current CTM and track per-path bbox +// m = [a, b, c, d, e, f] => x' = a*x + c*y + e, y' = b*x + d*y + f +EM_JS(void, js_rb_moveTo, (double x, double y), { + var r = Module._rb, m = r.s.ctm; + var tx = m[0] * x + m[2] * y + m[4]; + var ty = m[1] * x + m[3] * y + m[5]; + r.pd += 'M' + tx + ' ' + ty + ' '; + r.cx = tx; + r.cy = ty; + r.pathEmpty = false; + if (tx < r.pbbx0) + r.pbbx0 = tx; + if (ty < r.pbby0) + r.pbby0 = ty; + if (tx > r.pbbx1) + r.pbbx1 = tx; + if (ty > r.pbby1) + r.pbby1 = ty; +}); +EM_JS(void, js_rb_lineTo, (double x, double y), { + var r = Module._rb, m = r.s.ctm; + var tx = m[0] * x + m[2] * y + m[4]; + var ty = m[1] * x + m[3] * y + m[5]; + r.pd += 'L' + tx + ' ' + ty + ' '; + r.cx = tx; + r.cy = ty; + r.pathEmpty = false; + if (tx < r.pbbx0) + r.pbbx0 = tx; + if (ty < r.pbby0) + r.pbby0 = ty; + if (tx > r.pbbx1) + r.pbbx1 = tx; + if (ty > r.pbby1) + r.pbby1 = ty; +}); +EM_JS(void, js_rb_curveTo, (double x1, double y1, double x2, double y2, double x3, double y3), { + var r = Module._rb, m = r.s.ctm; + var tx1 = m[0] * x1 + m[2] * y1 + m[4], ty1 = m[1] * x1 + m[3] * y1 + m[5]; + var tx2 = m[0] * x2 + m[2] * y2 + m[4], ty2 = m[1] * x2 + m[3] * y2 + m[5]; + var tx3 = m[0] * x3 + m[2] * y3 + m[4], ty3 = m[1] * x3 + m[3] * y3 + m[5]; + r.pd += 'C' + tx1 + ' ' + ty1 + ' ' + tx2 + ' ' + ty2 + ' ' + tx3 + ' ' + ty3 + ' '; + r.cx = tx3; + r.cy = ty3; + r.pathEmpty = false; + var pts = [ tx1, ty1, tx2, ty2, tx3, ty3 ]; + for (var k = 0; k < 6; k += 2) + { + if (pts[k] < r.pbbx0) + r.pbbx0 = pts[k]; + if (pts[k + 1] < r.pbby0) + r.pbby0 = pts[k + 1]; + if (pts[k] > r.pbbx1) + r.pbbx1 = pts[k]; + if (pts[k + 1] > r.pbby1) + r.pbby1 = pts[k + 1]; + } +}); +EM_JS(void, js_rb_arc, (double cx, double cy, double rad, double a0, double a1, int ccw), { + var r = Module._rb, m = r.s.ctm; + if (rad < 0.001) + return; + + var da = a1 - a0; + if (ccw && da > 0) + da -= 2 * Math.PI; + if (!ccw && da < 0) + da += 2 * Math.PI; + if (Math.abs(da) < 1e-12) + return; + + function transformPoint(x, y) + { + return {x : m[0] * x + m[2] * y + m[4], y : m[1] * x + m[3] * y + m[5]}; + } + + function includePoint(p) + { + if (p.x < r.pbbx0) + r.pbbx0 = p.x; + if (p.y < r.pbby0) + r.pbby0 = p.y; + if (p.x > r.pbbx1) + r.pbbx1 = p.x; + if (p.y > r.pbby1) + r.pbby1 = p.y; + } + + var ax = m[0] * rad, ay = m[1] * rad; + var bx = m[2] * rad, by = m[3] * rad; + var q00 = ax * ax + bx * bx; + var q01 = ax * ay + bx * by; + var q11 = ay * ay + by * by; + var trace = q00 + q11; + var diff = q00 - q11; + var root = Math.sqrt(Math.max(0, diff * diff + 4 * q01 * q01)); + var rx = Math.sqrt(Math.max(0, (trace + root) / 2)); + var ry = Math.sqrt(Math.max(0, (trace - root) / 2)); + var rotation = 0.5 * Math.atan2(2 * q01, diff) * 180 / Math.PI; + var det = m[0] * m[3] - m[1] * m[2]; + + if (rx < 1e-9 || ry < 1e-9) + { + var first = transformPoint(cx + rad * Math.cos(a0), cy + rad * Math.sin(a0)); + var end = transformPoint(cx + rad * Math.cos(a1), cy + rad * Math.sin(a1)); + if (r.pathEmpty) + { + r.pd += 'M' + first.x + ' ' + first.y + ' '; + r.pathEmpty = false; + } + else + { + r.pd += 'L' + first.x + ' ' + first.y + ' '; + } + r.pd += 'L' + end.x + ' ' + end.y + ' '; + r.cx = end.x; + r.cy = end.y; + includePoint(first); + includePoint(end); + return; + } + + var first = transformPoint(cx + rad * Math.cos(a0), cy + rad * Math.sin(a0)); + if (r.pathEmpty) + { + r.pd += 'M' + first.x + ' ' + first.y + ' '; + r.pathEmpty = false; + } + else + { + r.pd += 'L' + first.x + ' ' + first.y + ' '; + } + + var segments = Math.max(1, Math.ceil(Math.abs(da) / Math.PI)); + var prev = a0; + for (var si = 1; si <= segments; si++) + { + var next = a0 + da * (si / segments); + var delta = next - prev; + var end = transformPoint(cx + rad * Math.cos(next), cy + rad * Math.sin(next)); + var largeArc = Math.abs(delta) > Math.PI ? 1 : 0; + var sweep = (delta >= 0) == (det >= 0) ? 1 : 0; + r.pd += 'A' + rx + ' ' + ry + ' ' + rotation + ' ' + largeArc + ' ' + sweep + ' ' + end.x + ' ' + end.y + ' '; + r.cx = end.x; + r.cy = end.y; + prev = next; + } + + var steps = Math.max(8, Math.ceil(Math.abs(da) / 0.02)); + for (var i = 0; i <= steps; i++) + { + var t = a0 + da * (i / steps); + includePoint(transformPoint(cx + rad * Math.cos(t), cy + rad * Math.sin(t))); + } +}); +EM_JS(void, js_rb_rect, (double x, double y, double w, double h), { + var r = Module._rb, m = r.s.ctm; + var x0 = x, y0 = y, x1 = x + w, y1 = y + h; + var t00x = m[0] * x0 + m[2] * y0 + m[4], t00y = m[1] * x0 + m[3] * y0 + m[5]; + var t10x = m[0] * x1 + m[2] * y0 + m[4], t10y = m[1] * x1 + m[3] * y0 + m[5]; + var t11x = m[0] * x1 + m[2] * y1 + m[4], t11y = m[1] * x1 + m[3] * y1 + m[5]; + var t01x = m[0] * x0 + m[2] * y1 + m[4], t01y = m[1] * x0 + m[3] * y1 + m[5]; + r.pd += 'M' + t00x + ' ' + t00y + 'L' + t10x + ' ' + t10y + 'L' + t11x + ' ' + t11y + 'L' + t01x + ' ' + t01y + 'Z '; + r.pathEmpty = false; + var pts = [ t00x, t00y, t10x, t10y, t11x, t11y, t01x, t01y ]; + for (var k = 0; k < 8; k += 2) + { + if (pts[k] < r.pbbx0) + r.pbbx0 = pts[k]; + if (pts[k + 1] < r.pbby0) + r.pbby0 = pts[k + 1]; + if (pts[k] > r.pbbx1) + r.pbbx1 = pts[k]; + if (pts[k + 1] > r.pbby1) + r.pbby1 = pts[k + 1]; + } +}); + +// -- Helpers -- +EM_JS(void, js_rb_emitPath, (int doFill, int doStroke), { + var r = Module._rb, s = r.s; + if (!r.pd) + return; + // Merge per-path bbox into global bbox (skip background-colored fill-only shapes) + var isBgFill = + doFill && !doStroke && r.bgColor && r.bgColor == 'rgb(' + Math.round(s.r * 255) + ',' + Math.round(s.g * 255) + ',' + Math.round(s.b * 255) + ')'; + if (!isBgFill) + { + if (r.pbbx0 < r.bbx0) + r.bbx0 = r.pbbx0; + if (r.pbby0 < r.bby0) + r.bby0 = r.pbby0; + if (r.pbbx1 > r.bbx1) + r.bbx1 = r.pbbx1; + if (r.pbby1 > r.bby1) + r.bby1 = r.pbby1; + } + // Reset per-path bbox for next path + r.pbbx0 = 1e9; + r.pbby0 = 1e9; + r.pbbx1 = -1e9; + r.pbby1 = -1e9; + var e = '> 3) + i]); + s.doff = off; +}); + +// -- Transform -- +EM_JS(void, js_rb_save, (), { + var r = Module._rb, s = r.s; + r.stack.push({ + ctm : s.ctm.slice(), + r : s.r, + g : s.g, + b : s.b, + a : s.a, + lw : s.lw, + lj : s.lj, + dash : s.dash.slice(), + doff : s.doff, + fsz : s.fsz, + ffam : s.ffam, + fbold : s.fbold, + fital : s.fital, + gradId : s.gradId + }); +}); +EM_JS(void, js_rb_restore, (), { + var r = Module._rb; + if (r.stack.length) + r.s = r.stack.pop(); +}); + +// Matrix multiply: result = current * [A,B,C,D,E,F] +EM_JS(void, js_rb_mmul, (double A, double B, double C, double D, double E, double F), { + var m = Module._rb.s.ctm; + var a = m[0], b = m[1], c = m[2], d = m[3], e = m[4], f = m[5]; + m[0] = a * A + c * B; + m[1] = b * A + d * B; + m[2] = a * C + c * D; + m[3] = b * C + d * D; + m[4] = a * E + c * F + e; + m[5] = b * E + d * F + f; +}); + +EM_JS(void, js_rb_setMatrix, (double a, double b, double c, double d, double e, double f), { + var m = Module._rb.s.ctm; + m[0] = a; + m[1] = b; + m[2] = c; + m[3] = d; + m[4] = e; + m[5] = f; +}); +EM_JS(void, js_rb_getMatrix, (double* out), { + var m = Module._rb.s.ctm; + HEAPF64[(out >> 3)] = m[0]; + HEAPF64[(out >> 3) + 1] = m[1]; + HEAPF64[(out >> 3) + 2] = m[2]; + HEAPF64[(out >> 3) + 3] = m[3]; + HEAPF64[(out >> 3) + 4] = m[4]; + HEAPF64[(out >> 3) + 5] = m[5]; +}); + +// -- Text -- +EM_JS(void, js_rb_setFont, (const char* fam, double sz, int bold, int ital), { + var s = Module._rb.s; + s.ffam = UTF8ToString(fam); + s.fsz = sz; + s.fbold = !!bold; + s.fital = !!ital; +}); +// clang-format off +EM_JS(void, js_rb_fillText, (const char* text, double x, double y), { + var r = Module._rb; + var s = r.s; + var m = s.ctm; + var t = UTF8ToString(text); + // Escape XML special chars + var esc = ''; + for (var i = 0; i < t.length; i++) + { + var c = t.charAt(i); + if (c == '&') + esc += '&'; + else if (c == '<') + esc += '<'; + else if (c == '>') + esc += '>'; + else if (c == '"') + esc += '"'; + else + esc += c; + } + t = esc; + // Track bbox: transform text anchor through CTM + var tx = m[0] * x + m[2] * y + m[4]; + var ty = m[1] * x + m[3] * y + m[5]; + // Measure text extent via Canvas2D for accurate bbox + var tw, th; + try { + var ctx = r._ncCtx || (typeof document != 'undefined' && document.createElement('canvas').getContext('2d')); + if (ctx) { + var qfam = s.ffam.split(',').map(function(f){return "'" + f.trim() + "'";}).join(', '); + ctx.font = (s.fital ? 'italic ' : '') + (s.fbold ? 'bold ' : '') + s.fsz + 'px ' + qfam; + tw = ctx.measureText(t).width * Math.abs(m[0]); + } else { + tw = t.length * s.fsz * 0.6 * Math.abs(m[0]); + } + } catch(ex) { + tw = t.length * s.fsz * 0.6 * Math.abs(m[0]); + } + th = s.fsz * Math.abs(m[3]); + if (tx < r.bbx0) + r.bbx0 = tx; + if (ty - th < r.bby0) + r.bby0 = ty - th; + if (tx + tw > r.bbx1) + r.bbx1 = tx + tw; + if (ty > r.bby1) + r.bby1 = ty; + var fc = 'rgb(' + Math.round(s.r * 255) + ',' + Math.round(s.g * 255) + ',' + Math.round(s.b * 255) + ')'; + var e = ''; + r.elems.push(e); +}); +// clang-format on + +// Text metrics approximation (works without Canvas2D) +// clang-format off +EM_JS(double, js_rb_measureWidth, (const char* text, double fontSize), { + var t = UTF8ToString(text); + var r = Module._rb; + var qfam = r.s.ffam.split(',').map(function(f) { return "'" + f.trim() + "'"; }).join(', '); + var style = (r.s.fital ? 'italic ' : '') + (r.s.fbold ? 'bold ' : ''); + // Measure at effective (CTM-scaled) font size to match SVG rendering + var scale = Math.abs(r.s.ctm[0]) || 1; + var effSize = fontSize * scale; + var font = style + effSize + 'px ' + qfam; + // Try browser Canvas2D + if (typeof document != 'undefined') + { + try + { + var c = document.createElement('canvas').getContext('2d'); + c.font = font; + return c.measureText(t).width / scale; + } + catch (e) + { + } + } + // Try node-canvas (Cairo-backed Canvas2D for Node.js) + if (typeof require !== 'undefined') + { + try + { + if (!r._ncCtx) + { + var createCanvas = require('canvas').createCanvas; + r._ncCtx = createCanvas(1, 1).getContext('2d'); + } + r._ncCtx.font = font; + return r._ncCtx.measureText(t).width / scale; + } + catch (e) + { + // node-canvas not installed, use fallback + } + } + // Fallback: approximate char widths for sans-serif (NotoSans-like) + var w = 0; + for (var i = 0; i < t.length; i++) + { + var ch = t.charCodeAt(i); + // CJK full-width characters + if ((ch >= 0x2E80 && ch <= 0x9FFF) || (ch >= 0xAC00 && ch <= 0xD7AF) || (ch >= 0xF900 && ch <= 0xFAFF) || (ch >= 0x3000 && ch <= 0x303F) || + (ch >= 0x3040 && ch <= 0x30FF) || (ch >= 0x31F0 && ch <= 0x31FF) || (ch >= 0xFF00 && ch <= 0xFFEF)) + w += 1.00; // CJK full-width + else if (ch >= 48 && ch <= 57) + w += 0.60; // digits + else if (ch == 77 || ch == 87) + w += 0.88; // M, W + else if (ch == 73 || ch == 108) + w += 0.33; // I, l + else if (ch >= 65 && ch <= 90) + w += 0.72; // uppercase + else if (ch >= 97 && ch <= 122) + w += 0.55; // lowercase + else if (ch == 32) + w += 0.30; // space + else if (ch == 43 || ch == 45) + w += 0.55; // +, - + // Greek, Cyrillic, symbols + else if (ch >= 0x0370 && ch <= 0x03FF) + w += 0.62; // Greek + else if (ch >= 0x0400 && ch <= 0x04FF) + w += 0.62; // Cyrillic + else + w += 0.60; + } + return w * fontSize; +}); +// clang-format on + +// Measure text ascent via Canvas2D (browser or node-canvas) +// clang-format off +EM_JS(double, js_rb_measureAscent, (const char* text, double fontSize), { + var t = UTF8ToString(text); + var r = Module._rb; + var qfam = r.s.ffam.split(',').map(function(f) { return "'" + f.trim() + "'"; }).join(', '); + var style = (r.s.fital ? 'italic ' : '') + (r.s.fbold ? 'bold ' : ''); + var font = style + fontSize + 'px ' + qfam; + // Try browser Canvas2D + if (typeof document != 'undefined') + { + try + { + var c = document.createElement('canvas').getContext('2d'); + c.font = font; + var m = c.measureText(t); + if (m.actualBoundingBoxAscent !== undefined) + return m.actualBoundingBoxAscent; + } + catch (e) + { + } + } + // Try node-canvas + if (typeof require !== 'undefined') + { + try + { + if (!r._ncCtx) + { + var createCanvas = require('canvas').createCanvas; + r._ncCtx = createCanvas(1, 1).getContext('2d'); + } + r._ncCtx.font = font; + var m = r._ncCtx.measureText(t); + if (m.actualBoundingBoxAscent !== undefined) + return m.actualBoundingBoxAscent; + } + catch (e) + { + } + } + return fontSize * 0.75; +}); +// clang-format on + +// -- Gradient -- +EM_JS(void, js_rb_setGradient, (double x0, double y0, double x1, double y1, double r1, double g1, double b1, double r2, double g2, double b2), { + var r = Module._rb; + var id = 'grad' + (r.gradCnt++); + var c1 = 'rgb(' + Math.round(r1 * 255) + ',' + Math.round(g1 * 255) + ',' + Math.round(b1 * 255) + ')'; + var c2 = 'rgb(' + Math.round(r2 * 255) + ',' + Math.round(g2 * 255) + ',' + Math.round(b2 * 255) + ')'; + r.defs += ''; + r.s.gradId = id; +}); + +// -- Image -- +// clang-format off +EM_JS(void, js_rb_drawImage, (const uint8_t* data, int dataLen, double x, double y, double w, double h), { + var r = Module._rb; + if (!r || !data || dataLen <= 0 || w <= 0 || h <= 0) + return; + + var ptr = data >>> 0; + + function bytesToBase64() + { + var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; + var out = ""; + var i = 0; + for (; i + 2 < dataLen; i += 3) + { + var n = (HEAPU8[ptr + i] << 16) | (HEAPU8[ptr + i + 1] << 8) | HEAPU8[ptr + i + 2]; + out += chars[(n >> 18) & 63] + chars[(n >> 12) & 63] + chars[(n >> 6) & 63] + chars[n & 63]; + } + if (i < dataLen) + { + var b0 = HEAPU8[ptr + i]; + var b1 = i + 1 < dataLen ? HEAPU8[ptr + i + 1] : 0; + var n = (b0 << 16) | (b1 << 8); + out += chars[(n >> 18) & 63] + chars[(n >> 12) & 63] + (i + 1 < dataLen ? chars[(n >> 6) & 63] : '=') + '='; + } + return out; + } + + function isSvg() + { + var i = 0; + if (dataLen >= 3 && HEAPU8[ptr] == 0xEF && HEAPU8[ptr + 1] == 0xBB && HEAPU8[ptr + 2] == 0xBF) + i = 3; + while (i < dataLen) + { + var ch = HEAPU8[ptr + i]; + if (ch != 9 && ch != 10 && ch != 13 && ch != 32) + break; + i++; + } + var header = ""; + var end = Math.min(dataLen, i + 512); + for (var j = i; j < end; j++) + { + var c = HEAPU8[ptr + j]; + if (c == 0) + break; + header += String.fromCharCode(c).toLowerCase(); + } + return header.indexOf(' r.bbx1) + r.bbx1 = px; + if (py > r.bby1) + r.bby1 = py; + } + + var m = r.s.ctm; + var pts = [ x, y, x + w, y, x + w, y + h, x, y + h ]; + for (var k = 0; k < pts.length; k += 2) + { + includePoint(m[0] * pts[k] + m[2] * pts[k + 1] + m[4], m[1] * pts[k] + m[3] * pts[k + 1] + m[5]); + } + + var mime = isSvg() ? 'image/svg+xml' : 'image/png'; + var href = 'data:' + mime + ';base64,' + bytesToBase64(); + var e = ' r.bbx0 && r.bby1 > r.bby0) + { + var pad = 2; + vx = r.bbx0 - pad; + vy = r.bby0 - pad; + vw = r.bbx1 - r.bbx0 + 2 * pad; + vh = r.bby1 - r.bby0 + 2 * pad; + } + var svg = ''; + if (r.defs) + svg += '' + r.defs + ''; + // Emit background rect at viewBox coordinates + if (r.bgColor) + svg += ''; + svg += r.elems.join(''); + svg += ''; + + // Helper: encode string to Uint8Array + function toBytes(s) + { + if (typeof TextEncoder != 'undefined') + return new TextEncoder().encode(s); + // Node.js fallback + var buf = new Uint8Array(s.length); + for (var i = 0; i < s.length; i++) + buf[i] = s.charCodeAt(i) & 0xFF; + return buf; + } + + if (mode == 1) + { // SVG - output SVG XML directly + r.out = toBytes(svg); + return r.out.length; + } + else if (mode == 0) + { // PNG - rasterize SVG via Canvas2D + // Node.js: use node-canvas (synchronous SVG-to-PNG) + if (typeof require != 'undefined') + { + try + { + var canvasMod = require('canvas'); + var img = new canvasMod.Image(); + img.src = Buffer.from(svg); + var pngCanvas = canvasMod.createCanvas(img.width, img.height); + var pngCtx = pngCanvas.getContext('2d'); + pngCtx.drawImage(img, 0, 0); + r.out = new Uint8Array(pngCanvas.toBuffer('image/png')); + return r.out.length; + } + catch (e) + { + } + } + // Browser fallback: output SVG bytes + r.out = toBytes(svg); + return r.out.length; + } + else + { // PDF - stub + r.out = toBytes('%PDF-1.4 stub'); + return r.out.length; + } +}); + +// clang-format off +EM_JS(void, js_rb_copyOutput, (uint8_t* dst, int len), { + var r = Module._rb; + if (!r || !r.out) + return; + HEAPU8.set(r.out.subarray(0, len), dst); +}); +// clang-format on + +EM_JS(int, js_rb_isPathEmpty, (), { return Module._rb ? Module._rb.pathEmpty : 1; }); + +// =================================================== +// JSRenderBackend implementation +// =================================================== + +JSRenderBackend::JSRenderBackend() : _width(0), _height(0), _mode(0), _fontSize(12), _fontBold(false), _fontItalic(false), _curX(0), _curY(0) +{ + strcpy(_fontFamily, "Noto Sans"); + _matrix[0] = 1; + _matrix[1] = 0; + _matrix[2] = 0; + _matrix[3] = 1; + _matrix[4] = 0; + _matrix[5] = 0; +} + +JSRenderBackend::~JSRenderBackend() +{ + js_rb_destroy(); +} + +void JSRenderBackend::createSurface(int mode, int width, int height, void*) +{ + _mode = mode; + _width = width; + _height = height; + js_rb_init(width, height, mode); +} + +void JSRenderBackend::closeSurface(int mode, bool discard, void* output) +{ + if (discard) + { + js_rb_destroy(); + return; + } + int rbMode = mode; // 0=PNG, 1=SVG, 2=PDF + int len = js_rb_finalize(rbMode); + if (len > 0 && output) + { + std::vector buf(len); + js_rb_copyOutput((uint8_t*)buf.data(), len); + Output* out = static_cast(output); + out->write(buf.data(), len); + } + js_rb_destroy(); +} + +void JSRenderBackend::createContext() +{ +} +void JSRenderBackend::destroyContext() +{ +} + +// Path +void JSRenderBackend::beginPath() +{ + js_rb_beginPath(); +} +void JSRenderBackend::closePath() +{ + js_rb_closePath(); +} +void JSRenderBackend::moveTo(double x, double y) +{ + _curX = x; + _curY = y; + js_rb_moveTo(x, y); +} +void JSRenderBackend::lineTo(double x, double y) +{ + _curX = x; + _curY = y; + js_rb_lineTo(x, y); +} +void JSRenderBackend::relMoveTo(double dx, double dy) +{ + _curX += dx; + _curY += dy; + js_rb_moveTo(_curX, _curY); +} +void JSRenderBackend::relLineTo(double dx, double dy) +{ + _curX += dx; + _curY += dy; + js_rb_lineTo(_curX, _curY); +} +void JSRenderBackend::curveTo(double x1, double y1, double x2, double y2, double x3, double y3) +{ + _curX = x3; + _curY = y3; + js_rb_curveTo(x1, y1, x2, y2, x3, y3); +} +void JSRenderBackend::arc(double cx, double cy, double r, double a0, double a1) +{ + js_rb_arc(cx, cy, r, a0, a1, 0); +} +void JSRenderBackend::arcNegative(double cx, double cy, double r, double a0, double a1) +{ + js_rb_arc(cx, cy, r, a0, a1, 1); +} +void JSRenderBackend::rect(double x, double y, double w, double h) +{ + js_rb_rect(x, y, w, h); +} + +// Drawing +void JSRenderBackend::fill() +{ + js_rb_fill(); +} +void JSRenderBackend::stroke() +{ + js_rb_stroke(); +} +void JSRenderBackend::paint() +{ + js_rb_paint(); +} + +// Style +void JSRenderBackend::setSourceRGB(double r, double g, double b) +{ + js_rb_setColor(r, g, b, 1); +} +void JSRenderBackend::setSourceRGBA(double r, double g, double b, double a) +{ + js_rb_setColor(r, g, b, a); +} +void JSRenderBackend::setLineWidth(double w) +{ + js_rb_setLineWidth(w); +} +void JSRenderBackend::setLineJoin(int j) +{ + js_rb_setLineJoin(j); +} +void JSRenderBackend::setDash(const double* p, int n, double off) +{ + js_rb_setDash(p, n, off); +} +void JSRenderBackend::setOperator(int) +{ +} // SVG doesn't need composite ops +void JSRenderBackend::setAntialias(int) +{ +} // SVG always anti-aliases + +// Transform +void JSRenderBackend::save() +{ + js_rb_save(); +} +void JSRenderBackend::restore() +{ + js_rb_restore(); +} +void JSRenderBackend::translate(double dx, double dy) +{ + js_rb_mmul(1, 0, 0, 1, dx, dy); +} +void JSRenderBackend::scale(double sx, double sy) +{ + js_rb_mmul(sx, 0, 0, sy, 0, 0); +} +void JSRenderBackend::rotate(double a) +{ + js_rb_mmul(cos(a), sin(a), -sin(a), cos(a), 0, 0); +} +void JSRenderBackend::getMatrix(double m[6]) +{ + js_rb_getMatrix(m); +} +void JSRenderBackend::setMatrix(const double m[6]) +{ + js_rb_setMatrix(m[0], m[1], m[2], m[3], m[4], m[5]); +} +void JSRenderBackend::initIdentityMatrix(double m[6]) +{ + m[0] = 1; + m[1] = 0; + m[2] = 0; + m[3] = 1; + m[4] = 0; + m[5] = 0; +} + +void JSRenderBackend::userToDevice(double& x, double& y) +{ + double m[6]; + js_rb_getMatrix(m); + double nx = m[0] * x + m[2] * y + m[4]; + double ny = m[1] * x + m[3] * y + m[5]; + x = nx; + y = ny; +} + +void JSRenderBackend::deviceToUser(double& x, double& y) +{ + double m[6]; + js_rb_getMatrix(m); + double det = m[0] * m[3] - m[1] * m[2]; + if (fabs(det) < 1e-12) + return; + double dx = x - m[4], dy = y - m[5]; + x = (m[3] * dx - m[2] * dy) / det; + y = (-m[1] * dx + m[0] * dy) / det; +} + +// Extents - approximate from path bounds +void JSRenderBackend::strokeExtents(double& x1, double& y1, double& x2, double& y2) +{ + x1 = 0; + y1 = 0; + x2 = _width; + y2 = _height; +} +void JSRenderBackend::pathExtents(double& x1, double& y1, double& x2, double& y2) +{ + x1 = 0; + y1 = 0; + x2 = _width; + y2 = _height; +} + +// Text +void JSRenderBackend::selectFontFace(const char* family, bool italic, bool bold) +{ + _fontItalic = italic; + _fontBold = bold; + strncpy(_fontFamily, family, sizeof(_fontFamily) - 1); + _fontFamily[sizeof(_fontFamily) - 1] = '\0'; + js_rb_setFont(_fontFamily, _fontSize, bold ? 1 : 0, italic ? 1 : 0); +} + +void JSRenderBackend::setFontSize(double size) +{ + _fontSize = size; + js_rb_setFont(_fontFamily, _fontSize, _fontBold ? 1 : 0, _fontItalic ? 1 : 0); +} + +void JSRenderBackend::textExtents(const char* text, double& width, double& height, double& x_bearing, double& y_bearing) +{ + width = js_rb_measureWidth(text, _fontSize); + double ascent = js_rb_measureAscent(text, _fontSize); + height = ascent; + x_bearing = 0; + y_bearing = -ascent; +} + +void JSRenderBackend::fontExtents(double& height) +{ + height = _fontSize; +} + +void JSRenderBackend::showText(const char* text) +{ + // SVG y is the baseline; offset from bounding box top by ascent + double ascent = js_rb_measureAscent(text, _fontSize); + js_rb_fillText(text, _curX, _curY + ascent); + // Advance current position + _curX += js_rb_measureWidth(text, _fontSize); +} + +void JSRenderBackend::textPath(const char* text) +{ + // In Cairo, textPath adds glyphs to the current path without rendering. + // In SVG, we must NOT emit a element here — this is only used + // for bounding box calculation (the "idle" pass in fontsDrawText). + // Only advance the cursor so that subsequent extent queries are correct. + _curX += js_rb_measureWidth(text, _fontSize); +} + +// Font options - no-op for SVG +void JSRenderBackend::createFontOptions() +{ +} +void JSRenderBackend::destroyFontOptions() +{ +} +void JSRenderBackend::setFontOptionsAntialias(int) +{ +} +void JSRenderBackend::applyFontOptions() +{ +} + +// Gradient +void JSRenderBackend::setLinearGradient(double x0, double y0, double x1, double y1, double r1, double g1, double b1, double r2, double g2, double b2) +{ + js_rb_setGradient(x0, y0, x1, y1, r1, g1, b1, r2, g2, b2); +} +void JSRenderBackend::clearPattern() +{ + js_rb_setColor(0, 0, 0, 1); +} + +// Image +void JSRenderBackend::drawPngImage(const void* data, int dataLen, double x, double y, double w, double h) +{ + js_rb_drawImage(static_cast(data), dataLen, x, y, w, h); +} +void JSRenderBackend::writeSurfaceToPng(void* output) +{ + int len = js_rb_finalize(0); // PNG mode + if (len > 0 && output) + { + std::vector buf(len); + js_rb_copyOutput((uint8_t*)buf.data(), len); + Output* out = static_cast(output); + out->write(buf.data(), len); + } +} + +bool JSRenderBackend::isPathEmpty() +{ + return js_rb_isPathEmpty(); +} +void JSRenderBackend::setSourceSurface(double, double) +{ +} + +#endif // __EMSCRIPTEN__ diff --git a/core/render2d/src/render_context.cpp b/core/render2d/src/render_context.cpp index ce69873369..0943702969 100644 --- a/core/render2d/src/render_context.cpp +++ b/core/render2d/src/render_context.cpp @@ -18,13 +18,19 @@ #include "render_context.h" #include "base_cpp/output.h" + +#ifdef __EMSCRIPTEN__ +#include "js_render_backend.h" +#else +#include "cairo_render_backend.h" +#endif #include "molecule/meta_commons.h" #include using namespace indigo; -std::mutex RenderContext::_cairo_mutex; +std::mutex RenderContext::_mutex; IMPL_ERROR(RenderContext, "render context"); @@ -33,17 +39,17 @@ IMPL_ERROR(RenderContext, "render context"); #include "cairo-win32.h" #include -cairo_surface_t* RenderContext::createWin32PrintingSurfaceForHDC() +void* RenderContext::createWin32PrintingSurfaceForHDC() { cairo_surface_t* surface = cairo_win32_printing_surface_create((HDC)opt.hdc); - cairoCheckStatus(); + backendCheckStatus(); return surface; } -cairo_surface_t* RenderContext::createWin32Surface() +void* RenderContext::createWin32Surface() { cairo_surface_t* surface = cairo_win32_surface_create((HDC)opt.hdc); - cairoCheckStatus(); + backendCheckStatus(); return surface; } @@ -65,7 +71,7 @@ static void _init_language_pack() } } -cairo_surface_t* RenderContext::createWin32PrintingSurfaceForMetafile(bool& isLarge) +void* RenderContext::createWin32PrintingSurfaceForMetafile(bool& isLarge) { HDC dc = GetDC(NULL); int hr = GetDeviceCaps(dc, HORZRES); @@ -85,19 +91,26 @@ cairo_surface_t* RenderContext::createWin32PrintingSurfaceForMetafile(bool& isLa _meta_hdc = CreateEnhMetaFileA(dc, 0, &rc, "Indigo Render2D\0\0"); ReleaseDC(NULL, dc); cairo_surface_t* s = cairo_win32_printing_surface_create((HDC)_meta_hdc); - cairoCheckStatus(); + backendCheckStatus(); StartPage((HDC)_meta_hdc); return s; } void RenderContext::storeAndDestroyMetafile(bool discard) { - cairo_surface_show_page(_surface); - cairoCheckStatus(); + auto* cairoBackend = dynamic_cast(_backend.get()); + cairo_surface_t* surface = cairoBackend ? cairoBackend->getSurface() : nullptr; + if (surface) + { + cairo_surface_show_page(surface); + backendCheckStatus(); + } EndPage((HDC)_meta_hdc); - cairo_surface_destroy(_surface); - cairoCheckStatus(); - _surface = NULL; + if (surface) + { + cairo_surface_destroy(surface); + backendCheckStatus(); + } HENHMETAFILE hemf = CloseEnhMetaFile((HDC)_meta_hdc); if (!discard) { @@ -120,8 +133,7 @@ void RenderContext::storeAndDestroyMetafile(bool discard) CP_DEF(RenderContext); RenderContext::RenderContext(const RenderOptions& ropt, float relativeThickness, float bondLineWidthFactor) - : CP_INIT, TL_CP_GET(_fontfamily), TL_CP_GET(transforms), metafileFontsToCurves(false), _cr(NULL), _surface(NULL), _meta_hdc(NULL), opt(ropt), - _pattern(NULL), _settings() + : CP_INIT, TL_CP_GET(_fontfamily), TL_CP_GET(transforms), metafileFontsToCurves(false), _meta_hdc(NULL), opt(ropt), _settings() { AcsOptions acs; if (ropt.fontSize > 0) @@ -138,16 +150,26 @@ RenderContext::RenderContext(const RenderOptions& ropt, float relativeThickness, acs.bondSpacing = ropt.bondSpacing; _settings.init(relativeThickness, bondLineWidthFactor, &acs); +#ifdef __EMSCRIPTEN__ + bprintf(_fontfamily, "Noto Sans"); +#else bprintf(_fontfamily, "Arial"); +#endif bbmin.x = bbmin.y = 1; bbmax.x = bbmax.y = -1; _defaultScale = 0.0f; + +#ifdef __EMSCRIPTEN__ + _backend = std::make_unique(); +#else + _backend = std::make_unique(); +#endif } void RenderContext::bbIncludePoint(const Vec2f& v) { double x = v.x, y = v.y; - cairo_user_to_device(_cr, &x, &y); + _backend->userToDevice(x, y); Vec2f u((float)x, (float)y); if (bbmin.x > bbmax.x) { // init @@ -164,7 +186,7 @@ void RenderContext::bbIncludePoint(const Vec2f& v) void RenderContext::_bbVecToUser(Vec2f& d, const Vec2f& s) { double x = s.x, y = s.y; - cairo_device_to_user(_cr, &x, &y); + _backend->deviceToUser(x, y); d.set((float)x, (float)y); } @@ -188,9 +210,9 @@ void RenderContext::bbIncludePath(bool stroke) { double x1, x2, y1, y2; if (stroke) - cairo_stroke_extents(_cr, &x1, &y1, &x2, &y2); + _backend->strokeExtents(x1, y1, x2, y2); else - cairo_path_extents(_cr, &x1, &y1, &x2, &y2); + _backend->pathExtents(x1, y1, x2, y2); bbIncludePoint(x1, y1); bbIncludePoint(x2, y2); } @@ -212,121 +234,59 @@ int RenderContext::getMaxPageSize() const return INT_MAX; } -cairo_status_t RenderContext::writer(void* closure, const unsigned char* data, unsigned int length) +void RenderContext::createSurface(int /*width*/, int /*height*/) { - try - { - ((Output*)closure)->write(data, length); - } - catch (Output::Error&) - { - return CAIRO_STATUS_WRITE_ERROR; - } - return CAIRO_STATUS_SUCCESS; -} - -void RenderContext::createSurface(cairo_write_func_t writer, Output* /*output*/, int /*width*/, int /*height*/) -{ - int mode = opt.mode; - if (writer == NULL && (mode == MODE_HDC || mode == MODE_PRN)) - { - mode = MODE_PDF; - } - + int rbMode = RBMODE_PNG; + switch (opt.mode) { - std::lock_guard _lock(_cairo_mutex); - switch (mode) - { - case MODE_NONE: - throw Error("mode not set"); - case MODE_PDF: - _surface = cairo_pdf_surface_create_for_stream(writer, opt.output, _width, _height); - cairoCheckSurfaceStatus(); - break; - case MODE_SVG: - _surface = cairo_svg_surface_create_for_stream(writer, opt.output, _width, _height); - cairoCheckSurfaceStatus(); - break; - case MODE_PNG: - _surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, _width, _height); - cairoCheckSurfaceStatus(); - break; - case MODE_HDC: -#ifdef _WIN32 - _surface = createWin32Surface(); -#else - throw Error("mode \"HDC\" is not supported on this platform"); -#endif - break; - case MODE_PRN: -#ifdef _WIN32 - _surface = createWin32PrintingSurfaceForHDC(); -#else - throw Error("mode \"PRN\" is not supported on this platform"); -#endif - break; - case MODE_EMF: -#ifdef _WIN32 - bool isLarge; - _surface = createWin32PrintingSurfaceForMetafile(isLarge); - if (isLarge) - metafileFontsToCurves = true; -#else - throw Error("mode \"EMF\" is not supported on this platform"); -#endif - break; - default: - throw Error("unknown mode: %d", mode); - } + case MODE_NONE: + throw Error("mode not set"); + case MODE_PDF: + rbMode = RBMODE_PDF; + break; + case MODE_SVG: + rbMode = RBMODE_SVG; + break; + case MODE_PNG: + rbMode = RBMODE_PNG; + break; + default: + rbMode = RBMODE_PNG; + break; } + _backend->createSurface(rbMode, _width, _height, opt.output); } -void RenderContext::cairoCheckSurfaceStatus() const +void RenderContext::backendCheckStatus() const { - cairo_status_t s; - s = cairo_surface_status(_surface); - if (s != CAIRO_STATUS_SUCCESS) - throw Error("Cairo error: %s\n", cairo_status_to_string(s)); + // Status checking is handled internally by each backend } void RenderContext::init() { fontsInit(); - cairo_text_extents_t te; - - { - std::lock_guard _lock(_cairo_mutex); - cairo_select_font_face(_cr, _fontfamily.ptr(), CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL); - cairoCheckStatus(); - cairo_set_font_size(_cr, _settings.fzz[FONT_SIZE_ATTR]); - cairoCheckStatus(); - cairo_text_extents(_cr, "N", &te); - cairoCheckStatus(); - } - - cairo_set_antialias(_cr, CAIRO_ANTIALIAS_GRAY); - cairoCheckStatus(); + _backend->selectFontFace(_fontfamily.ptr(), false, false); + _backend->setFontSize(_settings.fzz[FONT_SIZE_ATTR]); + _backend->setAntialias(0); _currentLineWidth = _settings.bondLineWidth; } void RenderContext::fillBackground() { - cairo_set_source_rgb(_cr, opt.backgroundColor.x, opt.backgroundColor.y, opt.backgroundColor.z); - cairoCheckStatus(); - cairo_paint(_cr); - cairoCheckStatus(); + _backend->setSourceRGB(opt.backgroundColor.x, opt.backgroundColor.y, opt.backgroundColor.z); + backendCheckStatus(); + _backend->paint(); + backendCheckStatus(); } void RenderContext::initNullContext() { _width = 10; _height = 10; - if (_surface != NULL || _cr != NULL) - throw Error("context is already open (or invalid)"); - createSurface(NULL, NULL, 1, 1); - cairoCheckStatus(); - _cr = cairo_create(_surface); + // Backend manages its own surface/context state + createSurface(1, 1); + _backend->createContext(); scale(_defaultScale); } @@ -336,52 +296,35 @@ void RenderContext::initContext(int width, int height) _height = height; if (opt.mode != MODE_HDC && opt.mode != MODE_PRN && opt.output == NULL) throw Error("output not set"); - if (_surface != NULL || _cr != NULL) - throw Error("context is already open (or invalid)"); + // Backend manages its own surface/context state - createSurface(writer, opt.output, _width, _height); - _cr = cairo_create(_surface); + createSurface(_width, _height); + _backend->createContext(); if (opt.backgroundColor.x >= 0 && opt.backgroundColor.y >= 0 && opt.backgroundColor.z >= 0) fillBackground(); } void RenderContext::closeContext(bool discard) { - if (_cr != NULL) - { - std::lock_guard _lock(_cairo_mutex); - cairo_destroy(_cr); - _cr = NULL; - } + _backend->destroyContext(); + int rbMode = RBMODE_PNG; switch (opt.mode) { - case MODE_NONE: - throw Error("mode not set"); - case MODE_PNG: - if (!discard) - cairo_surface_write_to_png_stream(_surface, writer, opt.output); - break; case MODE_PDF: + rbMode = RBMODE_PDF; + break; case MODE_SVG: - case MODE_HDC: - case MODE_PRN: + rbMode = RBMODE_SVG; break; - case MODE_EMF: -#ifdef _WIN32 - storeAndDestroyMetafile(discard); -#endif + case MODE_PNG: + rbMode = RBMODE_PNG; break; default: - throw Error("unknown mode: %d", opt.mode); - } - - if (_surface != NULL) - { - std::lock_guard _lock(_cairo_mutex); - cairo_surface_destroy(_surface); - _surface = NULL; + rbMode = RBMODE_PNG; + break; } + _backend->closeSurface(rbMode, discard, opt.output); bbmin.x = bbmin.y = 1; bbmax.x = bbmax.y = -1; @@ -391,29 +334,29 @@ void RenderContext::closeContext(bool discard) void RenderContext::translate(float dx, float dy) { - cairo_translate(_cr, dx, dy); - cairoCheckStatus(); + _backend->translate(dx, dy); + backendCheckStatus(); } void RenderContext::scale(float s) { - cairo_scale(_cr, s, s); - cairoCheckStatus(); + _backend->scale(s, s); + backendCheckStatus(); } void RenderContext::storeTransform() { - cairo_matrix_t& t = transforms.push(); - cairo_get_matrix(_cr, &t); - cairoCheckStatus(); + RenderMatrix& t = transforms.push(); + _backend->getMatrix(t.m); + backendCheckStatus(); } void RenderContext::restoreTransform() { - std::lock_guard _lock(_cairo_mutex); - cairo_matrix_t& t = transforms.top(); - cairo_set_matrix(_cr, &t); - cairoCheckStatus(); + std::lock_guard _lock(_mutex); + RenderMatrix& t = transforms.top(); + _backend->setMatrix(t.m); + backendCheckStatus(); } void RenderContext::removeStoredTransform() @@ -423,26 +366,26 @@ void RenderContext::removeStoredTransform() void RenderContext::resetTransform() { - cairo_matrix_t t; - cairo_matrix_init_identity(&t); - cairo_set_matrix(_cr, &t); - cairoCheckStatus(); + RenderMatrix t; + _backend->initIdentityMatrix(t.m); + _backend->setMatrix(t.m); + backendCheckStatus(); } void RenderContext::setLineWidth(double width) { _currentLineWidth = (float)width; - cairo_set_line_width(_cr, width); - cairoCheckStatus(); + _backend->setLineWidth(width); + backendCheckStatus(); } void RenderContext::drawRectangle(const Vec2f& p, const Vec2f& sz) { - cairo_rectangle(_cr, p.x, p.y, sz.x, sz.y); - cairoCheckStatus(); + _backend->rect(p.x, p.y, sz.x, sz.y); + backendCheckStatus(); checkPathNonEmpty(); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawEllipse(const Vec2f& v1, const Vec2f& v2) @@ -450,29 +393,29 @@ void RenderContext::drawEllipse(const Vec2f& v1, const Vec2f& v2) Rect2f bbox(v1, v2); auto width = bbox.width(); auto height = bbox.height(); - cairo_matrix_t save_matrix; - cairo_get_matrix(_cr, &save_matrix); - cairo_translate(_cr, bbox.center().x, bbox.center().y); - cairo_scale(_cr, 1, height / width); - cairo_translate(_cr, -bbox.center().x, -bbox.center().y); - cairo_arc(_cr, bbox.center().x, bbox.center().y, width / 2.0, 0, 2 * M_PI); - cairo_set_matrix(_cr, &save_matrix); + RenderMatrix save_matrix; + _backend->getMatrix(save_matrix.m); + _backend->translate(bbox.center().x, bbox.center().y); + _backend->scale(1, height / width); + _backend->translate(-bbox.center().x, -bbox.center().y); + _backend->arc(bbox.center().x, bbox.center().y, width / 2.0, 0, 2 * M_PI); + _backend->setMatrix(save_matrix.m); checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } void RenderContext::drawItemBackground(const RenderItem& item) { - cairo_rectangle(_cr, item.bbp.x, item.bbp.y, item.bbsz.x, item.bbsz.y); - cairoCheckStatus(); + _backend->rect(item.bbp.x, item.bbp.y, item.bbsz.x, item.bbsz.y); + backendCheckStatus(); if (opt.backgroundColor.x >= 0 && opt.backgroundColor.y >= 0 && opt.backgroundColor.z >= 0) { setSingleSource(opt.backgroundColor); checkPathNonEmpty(); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } else { @@ -481,16 +424,16 @@ void RenderContext::drawItemBackground(const RenderItem& item) * Fill the rectangle with the transparent color, invalidating it and * erasing everything underneath */ - cairo_save(_cr); - cairoCheckStatus(); - cairo_set_source_rgba(_cr, 0, 0, 0, 0); - cairoCheckStatus(); - cairo_set_operator(_cr, CAIRO_OPERATOR_SOURCE); - cairoCheckStatus(); - cairo_fill(_cr); - cairoCheckStatus(); - cairo_restore(_cr); - cairoCheckStatus(); + _backend->save(); + backendCheckStatus(); + _backend->setSourceRGBA(0, 0, 0, 0); + backendCheckStatus(); + _backend->setOperator(1); + backendCheckStatus(); + _backend->fill(); + backendCheckStatus(); + _backend->restore(); + backendCheckStatus(); return; } } @@ -525,47 +468,9 @@ void RenderContext::drawTextItemText(const TextItem& ti, const Vec3f& color, boo fontsDrawText(ti_mod, color, idle); } -struct PngReadContext -{ - const unsigned char* data; - size_t size; - size_t offset; -}; - -cairo_status_t pngReadFunc(void* closure, unsigned char* data, unsigned int length) -{ - PngReadContext* context = static_cast(closure); - if (context->offset + length > context->size) - return CAIRO_STATUS_READ_ERROR; - memcpy(data, context->data + context->offset, length); - context->offset += length; - return CAIRO_STATUS_SUCCESS; -} - void RenderContext::drawPng(const std::string& pngData, const Rect2f& bbox) { - PngReadContext context = {(const unsigned char*)pngData.data(), pngData.size(), 0}; - cairo_surface_t* image = cairo_image_surface_create_from_png_stream(pngReadFunc, &context); - - if (cairo_surface_status(image) != CAIRO_STATUS_SUCCESS) - { - cairo_surface_destroy(image); - return; - } - - double imgWidth = cairo_image_surface_get_width(image); - double imgHeight = cairo_image_surface_get_height(image); - - cairo_save(_cr); - - cairo_translate(_cr, bbox.left(), bbox.bottom()); - cairo_scale(_cr, bbox.width() / imgWidth, bbox.height() / imgHeight); - - cairo_set_source_surface(_cr, image, 0, 0); - cairo_paint(_cr); - - cairo_restore(_cr); - cairo_surface_destroy(image); + _backend->drawPngImage(pngData.data(), static_cast(pngData.size()), bbox.left(), bbox.bottom(), bbox.width(), bbox.height()); bbIncludePoint(bbox.leftTop()); bbIncludePoint(bbox.rightBottom()); } @@ -577,10 +482,10 @@ void RenderContext::drawLine(const Vec2f& v0, const Vec2f& v1) checkPathNonEmpty(); bbIncludePath(true); { - std::lock_guard _lock(_cairo_mutex); - cairo_stroke(_cr); + std::lock_guard _lock(_mutex); + _backend->stroke(); } - cairoCheckStatus(); + backendCheckStatus(); } void RenderContext::drawPoly(const Array& v) @@ -591,19 +496,15 @@ void RenderContext::drawPoly(const Array& v) lineTo(v[0]); checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } void RenderContext::checkPathNonEmpty() const { #ifdef DEBUG - cairo_path_t* p = cairo_copy_path(_cr); - cairoCheckStatus(); - if (p->num_data == 0) + if (_backend->isPathEmpty()) throw Error("Empty path"); - cairo_path_destroy(p); - cairoCheckStatus(); #endif } @@ -615,8 +516,8 @@ void RenderContext::fillQuad(const Vec2f& v0, const Vec2f& v1, const Vec2f& v2, lineTo(v3); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::fillHex(const Vec2f& v0, const Vec2f& v1, const Vec2f& v2, const Vec2f& v3, const Vec2f& v4, const Vec2f& v5) @@ -629,8 +530,8 @@ void RenderContext::fillHex(const Vec2f& v0, const Vec2f& v1, const Vec2f& v2, c lineTo(v5); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::fillQuadStripes(const Vec2f& v0r, const Vec2f& v0l, const Vec2f& v1r, const Vec2f& v1l, int cnt) @@ -653,8 +554,8 @@ void RenderContext::fillQuadStripes(const Vec2f& v0r, const Vec2f& v0l, const Ve } checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } void RenderContext::fillQuadStripesSpacing(const Vec2f& v0r, const Vec2f& v0l, const Vec2f& v1r, const Vec2f& v1l, float spacing) @@ -684,8 +585,8 @@ void RenderContext::fillQuadStripesSpacing(const Vec2f& v0r, const Vec2f& v0l, c } checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } void RenderContext::fillPentagon(const Vec2f& v0, const Vec2f& v1, const Vec2f& v2, const Vec2f& v3, const Vec2f& v4) @@ -697,8 +598,8 @@ void RenderContext::fillPentagon(const Vec2f& v0, const Vec2f& v1, const Vec2f& lineTo(v4); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawQuad(const Vec2f& v0, const Vec2f& v1, const Vec2f& v2, const Vec2f& v3) @@ -707,12 +608,12 @@ void RenderContext::drawQuad(const Vec2f& v0, const Vec2f& v1, const Vec2f& v2, lineTo(v1); lineTo(v2); lineTo(v3); - cairo_close_path(_cr); - cairoCheckStatus(); + _backend->closePath(); + backendCheckStatus(); checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } void RenderContext::drawTriangleZigzag(const Vec2f& v0, const Vec2f& v1, const Vec2f& v2, int cnt) @@ -724,8 +625,8 @@ void RenderContext::drawTriangleZigzag(const Vec2f& v0, const Vec2f& v1, const V dl.diff(v2, v0); dl.scale(1.0f / cnt); - cairo_set_line_join(_cr, CAIRO_LINE_JOIN_MITER); - cairoCheckStatus(); + _backend->setLineJoin(0); + backendCheckStatus(); moveTo(v0); if (cnt < 3) @@ -741,47 +642,47 @@ void RenderContext::drawTriangleZigzag(const Vec2f& v0, const Vec2f& v1, const V } checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); - cairo_set_line_join(_cr, CAIRO_LINE_JOIN_BEVEL); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); + _backend->setLineJoin(2); + backendCheckStatus(); } void RenderContext::drawCircle(const Vec2f& center, const float r) { - cairo_new_path(_cr); - arc(_cr, center.x, center.y, r, 0, 2 * M_PI); - cairoCheckStatus(); + _backend->beginPath(); + _arc(center.x, center.y, r, 0, 2 * M_PI); + backendCheckStatus(); checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); - cairo_new_path(_cr); + _backend->stroke(); + backendCheckStatus(); + _backend->beginPath(); } void RenderContext::fillCircle(const Vec2f& center, const float r) { - arc(_cr, center.x, center.y, r, 0, 2 * M_PI); - cairoCheckStatus(); + _arc(center.x, center.y, r, 0, 2 * M_PI); + backendCheckStatus(); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawArc(const Vec2f& center, const float r, const float a0, const float a1) { - cairo_new_path(_cr); - cairoCheckStatus(); - arc(_cr, center.x, center.y, r, a0, a1); - cairoCheckStatus(); + _backend->beginPath(); + backendCheckStatus(); + _arc(center.x, center.y, r, a0, a1); + backendCheckStatus(); checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } -void RenderContext::arc(cairo_t* cr, double xc, double yc, double radius, double angle1, double angle2) +void RenderContext::_arc(double xc, double yc, double radius, double angle1, double angle2) { #ifdef __EMSCRIPTEN__ // In the WASM build this workaround fixes function signature issues with cairo_arc. @@ -795,27 +696,27 @@ void RenderContext::arc(cairo_t* cr, double xc, double yc, double radius, double { Vec2f p(radius * cos(phi) + xc, radius * sin(phi) + yc); if (i) - cairo_line_to(cr, p.x, p.y); + _backend->lineTo(p.x, p.y); else - cairo_move_to(cr, p.x, p.y); + _backend->moveTo(p.x, p.y); phi += step; } #else - cairo_arc(cr, xc, yc, radius, angle1, angle2); + _backend->arc(xc, yc, radius, angle1, angle2); #endif } void RenderContext::setFontSize(double fontSize) { - cairo_set_font_size(_cr, fontSize); - cairoCheckStatus(); + _backend->setFontSize(fontSize); + backendCheckStatus(); } double RenderContext::getFontExtentHeight() { - cairo_font_extents_t fe; - cairo_font_extents(_cr, &fe); - return fe.height; + double h; + _backend->fontExtents(h); + return h; } void RenderContext::setTextItemSize(TextItem& ti) @@ -823,7 +724,7 @@ void RenderContext::setTextItemSize(TextItem& ti) if (!ti.bold) ti.bold = ti.highlighted && opt.highlightThicknessEnable; fontsSetFont(ti); - fontsGetTextExtents(_cr, ti.text.ptr(), ti.fontsize, ti.bbsz.x, ti.bbsz.y, ti.relpos.x, ti.relpos.y); + fontsGetTextExtents(ti.text.ptr(), ti.fontsize, ti.bbsz.x, ti.bbsz.y, ti.relpos.x, ti.relpos.y); } void RenderContext::setTextItemSize(TextItem& ti, const Vec2f& c) @@ -864,8 +765,8 @@ void RenderContext::drawAttachmentPoint(RenderItemAttachmentPoint& ri, bool idle lineTo(ri.p1); checkPathNonEmpty(); bbIncludePath(false); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); Vec2f n; n.copy(ri.dir); @@ -886,12 +787,12 @@ void RenderContext::drawAttachmentPoint(RenderItemAttachmentPoint& ri, bool idle p.addScaled(n, step); r.lineCombin(p, ri.dir, waveWidth * turn); r.addScaled(n, -waveWidth * slopeFactor); - cairo_curve_to(_cr, q.x, q.y, r.x, r.y, p.x, p.y); + _backend->curveTo(q.x, q.y, r.x, r.y, p.x, p.y); } checkPathNonEmpty(); bbIncludePath(false); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); QS_DEF(TextItem, ti); ti.clear(); @@ -946,8 +847,8 @@ void RenderContext::_drawGraphItem(GraphItem& gi) break; case GraphItem::DOT: moveTo(v0); - arc(_cr, v0.x, v0.y, _settings.graphItemDotRadius, 0, 2 * M_PI); - cairoCheckStatus(); + _arc(v0.x, v0.y, _settings.graphItemDotRadius, 0, 2 * M_PI); + backendCheckStatus(); break; case GraphItem::PLUS: moveTo(v0); @@ -974,8 +875,8 @@ void RenderContext::_drawGraphItem(GraphItem& gi) } checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawBracket(RenderItemBracket& bracket) @@ -991,17 +892,17 @@ void RenderContext::drawBracket(RenderItemBracket& bracket) checkPathNonEmpty(); bbIncludePath(false); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } void RenderContext::fillRect(double x, double y, double w, double h) { - cairo_rectangle(_cr, x, y, w, h); - cairoCheckStatus(); + _backend->rect(x, y, w, h); + backendCheckStatus(); checkPathNonEmpty(); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawEquality(const Vec2f& pos, const float linewidth, const float size, const float interval) @@ -1015,8 +916,8 @@ void RenderContext::drawEquality(const Vec2f& pos, const float linewidth, const setLineWidth(linewidth); checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } void RenderContext::drawPlus(const Vec2f& pos, const float linewidth, const float size) @@ -1031,8 +932,8 @@ void RenderContext::drawPlus(const Vec2f& pos, const float linewidth, const floa setLineWidth(linewidth); checkPathNonEmpty(); bbIncludePath(true); - cairo_stroke(_cr); - cairoCheckStatus(); + _backend->stroke(); + backendCheckStatus(); } void RenderContext::drawHalfEllipse(const Vec2f& v1, const Vec2f& v2, const float height, const bool is_negative) @@ -1043,17 +944,17 @@ void RenderContext::drawHalfEllipse(const Vec2f& v1, const Vec2f& v2, const floa Vec2f d; d.diff(v2, v1); float width = d.length(); - cairo_matrix_t save_matrix; - cairo_get_matrix(_cr, &save_matrix); - cairo_translate(_cr, (v1.x + v2.x) / 2.0, (v1.y + v2.y) / 2.0); - cairo_rotate(_cr, atan2(d.y, d.x)); - cairo_scale(_cr, 1, 2 * h / width); - cairo_translate(_cr, -(v1.x + v2.x) / 2.0, -(v1.y + v2.y) / 2.0); + RenderMatrix save_matrix; + _backend->getMatrix(save_matrix.m); + _backend->translate((v1.x + v2.x) / 2.0, (v1.y + v2.y) / 2.0); + _backend->rotate(atan2(d.y, d.x)); + _backend->scale(1, 2 * h / width); + _backend->translate(-(v1.x + v2.x) / 2.0, -(v1.y + v2.y) / 2.0); if (is_negative) - cairo_arc_negative(_cr, (v1.x + v2.x) / 2.0, (v1.y + v2.y) / 2.0, width / 2.0, angle1, angle2); + _backend->arcNegative((v1.x + v2.x) / 2.0, (v1.y + v2.y) / 2.0, width / 2.0, angle1, angle2); else - cairo_arc(_cr, (v1.x + v2.x) / 2.0, (v1.y + v2.y) / 2.0, width / 2.0, angle1, angle2); - cairo_set_matrix(_cr, &save_matrix); + _backend->arc((v1.x + v2.x) / 2.0, (v1.y + v2.y) / 2.0, width / 2.0, angle1, angle2); + _backend->setMatrix(save_matrix.m); } void RenderContext::drawTriangleArrowHeader(const Vec2f& v, const Vec2f& dir, const float /*width*/, const float headwidth, const float headsize) @@ -1181,7 +1082,7 @@ void RenderContext::drawEllipticalArrow(const Vec2f& p1, const Vec2f& p2, const drawHalfArrowHeader(p2, n_orig, width, headwidth, headsize); break; } - cairo_fill(_cr); + _backend->fill(); pb.addScaled(d, width / 2); // go forward to outer ellipse d.negate(); // backward pa.addScaled(d, width / 2); // back to outer ellipse @@ -1195,8 +1096,8 @@ void RenderContext::drawEllipticalArrow(const Vec2f& p1, const Vec2f& p2, const drawHalfEllipse(pb, pa, height - width * h_sign, true); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawBothEndsArrow(const Vec2f& p1, const Vec2f& p2, const float width, const float headwidth, const float headsize) @@ -1242,8 +1143,8 @@ void RenderContext::drawBothEndsArrow(const Vec2f& p1, const Vec2f& p2, const fl lineTo(p); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawDashedArrow(const Vec2f& p1, const Vec2f& p2, const float width, const float headwidth, const float headsize) @@ -1290,8 +1191,8 @@ void RenderContext::drawDashedArrow(const Vec2f& p1, const Vec2f& p2, const floa drawArrowHeader(p2, d, width, headwidth, headsize, false); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawBar(const Vec2f& p1, const Vec2f& p2, const float width, const float margin) @@ -1356,8 +1257,8 @@ void RenderContext::drawEquillibriumHalf(const Vec2f& p1, const Vec2f& p2, const drawBar(pb, pa, width, margin); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawEquillibriumFilledTriangle(const Vec2f& p1, const Vec2f& p2, const float width, const float headwidth, const float headsize) @@ -1378,8 +1279,8 @@ void RenderContext::drawEquillibriumFilledTriangle(const Vec2f& p1, const Vec2f& drawCustomArrow(pb, pa, width, headwidth, headsize, false); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawCustomArrow(const Vec2f& p1, const Vec2f& p2, const float width, const float headwidth, const float headsize, const bool is_bow) @@ -1452,8 +1353,8 @@ void RenderContext::drawRetroSynthArrow(const Vec2f& p1, const Vec2f& p2, const drawArrowHeader(pb, d, width, (headwidth + width) * 2, headsize * 2); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawCustomArrow(const Vec2f& p1, const Vec2f& p2, const float width, const float headwidth, const float headsize, const bool is_bow, @@ -1466,7 +1367,7 @@ void RenderContext::drawCustomArrow(const Vec2f& p1, const Vec2f& p2, const floa drawCustomArrow(p1, p2, width, headwidth, headsize, is_bow); if (is_failed) { - cairo_fill(_cr); + _backend->fill(); for (int arr_ind = 0; arr_ind < 2; ++arr_ind) { p.set(p1.x, p1.y); @@ -1495,8 +1396,8 @@ void RenderContext::drawCustomArrow(const Vec2f& p1, const Vec2f& p2, const floa } checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } void RenderContext::drawArrow(const Vec2f& p1, const Vec2f& p2, const float width, const float headwidth, const float headsize) @@ -1525,8 +1426,8 @@ void RenderContext::drawArrow(const Vec2f& p1, const Vec2f& p2, const float widt lineTo(p); checkPathNonEmpty(); bbIncludePath(false); - cairo_fill(_cr); - cairoCheckStatus(); + _backend->fill(); + backendCheckStatus(); } float RenderContext::highlightedBondLineWidth() const @@ -1574,39 +1475,26 @@ void RenderContext::setSingleSource(int color) { Vec3f v; getColorVec(v, color); - cairo_set_source_rgb(_cr, v.x, v.y, v.z); - cairoCheckStatus(); + _backend->setSourceRGB(v.x, v.y, v.z); + backendCheckStatus(); } void RenderContext::setSingleSource(const Vec3f& color) { - cairo_set_source_rgb(_cr, color.x, color.y, color.z); - cairoCheckStatus(); + _backend->setSourceRGB(color.x, color.y, color.z); + backendCheckStatus(); } void RenderContext::setGradientSource(const Vec3f& color1, const Vec3f& color2, const Vec2f& pos1, const Vec2f& pos2) { - if (_pattern != NULL) - { - throw new Error("Pattern already initialized"); - } - - _pattern = cairo_pattern_create_linear(pos1.x, pos1.y, pos2.x, pos2.y); - cairo_pattern_add_color_stop_rgb(_pattern, 0, color1.x, color1.y, color1.z); - cairo_pattern_add_color_stop_rgb(_pattern, 1, color2.x, color2.y, color2.z); - cairo_set_source(_cr, _pattern); + _backend->setLinearGradient(pos1.x, pos1.y, pos2.x, pos2.y, color1.x, color1.y, color1.z, color2.x, color2.y, color2.z); - cairoCheckStatus(); + backendCheckStatus(); } void RenderContext::clearPattern() { - if (_pattern != NULL) - { - cairo_pattern_destroy(_pattern); - _pattern = NULL; - cairoCheckStatus(); - } + _backend->clearPattern(); } float RenderContext::_getDashedLineAlignmentOffset(float length) @@ -1622,50 +1510,50 @@ float RenderContext::_getDashedLineAlignmentOffset(float length) void RenderContext::setDash(const Array& dash, float length) { - cairo_set_dash(_cr, dash.ptr(), dash.size(), _getDashedLineAlignmentOffset(length)); - cairoCheckStatus(); + _backend->setDash(dash.ptr(), dash.size(), _getDashedLineAlignmentOffset(length)); + backendCheckStatus(); } void RenderContext::resetDash() { - cairo_set_dash(_cr, NULL, 0, 0); - cairoCheckStatus(); + _backend->setDash(nullptr, 0, 0); + backendCheckStatus(); } void RenderContext::lineTo(const Vec2f& v) { - cairo_line_to(_cr, v.x, v.y); - cairoCheckStatus(); + _backend->lineTo(v.x, v.y); + backendCheckStatus(); } void RenderContext::lineToRel(float x, float y) { - cairo_rel_line_to(_cr, x, y); - cairoCheckStatus(); + _backend->relLineTo(x, y); + backendCheckStatus(); } void RenderContext::lineToRel(const Vec2f& v) { - cairo_rel_line_to(_cr, v.x, v.y); - cairoCheckStatus(); + _backend->relLineTo(v.x, v.y); + backendCheckStatus(); } void RenderContext::moveTo(const Vec2f& v) { - cairo_move_to(_cr, v.x, v.y); - cairoCheckStatus(); + _backend->moveTo(v.x, v.y); + backendCheckStatus(); } void RenderContext::moveToRel(float x, float y) { - cairo_rel_move_to(_cr, x, y); - cairoCheckStatus(); + _backend->relMoveTo(x, y); + backendCheckStatus(); } void RenderContext::moveToRel(const Vec2f& v) { - cairo_rel_move_to(_cr, v.x, v.y); - cairoCheckStatus(); + _backend->relMoveTo(v.x, v.y); + backendCheckStatus(); } int RenderContext::getElementColor(int label) diff --git a/core/render2d/src/render_fonts.cpp b/core/render2d/src/render_fonts.cpp index ba706f0fe2..365797194f 100644 --- a/core/render2d/src/render_fonts.cpp +++ b/core/render2d/src/render_fonts.cpp @@ -17,6 +17,7 @@ ***************************************************************************/ #include "base_cpp/output.h" +#include "font_lang_detector.h" #include "math/algebra.h" #include "render_context.h" @@ -26,70 +27,29 @@ using namespace indigo; -void RenderContext::cairoCheckStatus() const -{ -#ifdef DEBUG - cairo_status_t s; - if (_cr) - { - s = cairo_status(_cr); - if (s != CAIRO_STATUS_SUCCESS /*&& s <= CAIRO_STATUS_INVALID_WEIGHT*/) - throw Error("Cairo error: %i -- %s\n", s, cairo_status_to_string(s)); - } -#endif -} - void RenderContext::fontsClear() { - memset(_scaled_fonts, 0, FONT_SIZE_COUNT * 2 * sizeof(cairo_scaled_font_t*)); + // scaled fonts managed by backend + + // font faces managed by backend - cairoFontFaceRegular = NULL; - cairoFontFaceBold = NULL; - fontOptions = NULL; + // matrix init managed by backend + backendCheckStatus(); - cairo_matrix_init_identity(&fontCtm); - cairoCheckStatus(); - cairo_matrix_init_identity(&fontScale); - cairoCheckStatus(); + backendCheckStatus(); } void RenderContext::fontsInit() { fontsDispose(); - fontOptions = cairo_font_options_create(); - cairoCheckStatus(); - cairo_font_options_set_antialias(fontOptions, CAIRO_ANTIALIAS_GRAY); - cairoCheckStatus(); - cairo_set_font_options(_cr, fontOptions); - cairoCheckStatus(); + _backend->createFontOptions(); + _backend->setFontOptionsAntialias(0); + _backend->applyFontOptions(); } void RenderContext::fontsDispose() { - for (int i = 0; i < FONT_SIZE_COUNT * 2; ++i) - { - if (_scaled_fonts[i] != NULL) - { - cairo_scaled_font_destroy(_scaled_fonts[i]); - cairoCheckStatus(); - } - } - if (cairoFontFaceRegular != NULL) - { - cairo_font_face_destroy(cairoFontFaceRegular); - cairoCheckStatus(); - } - if (cairoFontFaceBold != NULL) - { - cairo_font_face_destroy(cairoFontFaceBold); - cairoCheckStatus(); - } - - if (fontOptions != NULL) - { - cairo_font_options_destroy(fontOptions); - cairoCheckStatus(); - } + _backend->destroyFontOptions(); fontsClear(); } @@ -105,51 +65,55 @@ double RenderContext::fontGetSize(FONT_SIZE size) #ifndef RENDER_USE_FONT_MANAGER void RenderContext::fontsSetFont(const TextItem& ti) { - std::lock_guard _lock(_cairo_mutex); - cairo_select_font_face(_cr, _fontfamily.ptr(), ti.italic ? CAIRO_FONT_SLANT_ITALIC : CAIRO_FONT_SLANT_NORMAL, - ti.bold ? CAIRO_FONT_WEIGHT_BOLD : CAIRO_FONT_WEIGHT_NORMAL); - - cairoCheckStatus(); - cairo_set_font_size(_cr, ti.size > 0 ? ti.size : fontGetSize(ti.fontsize)); - cairoCheckStatus(); +#if defined(__EMSCRIPTEN__) && defined(RENDER_ENABLE_CJK) + // For WASM/Emscripten builds with CJK enabled, detect CJK characters + // and select the appropriate Noto Sans CJK font family for the SVG output. + std::string family = _fontfamily.ptr(); + FontLangDetector detector; + auto lang = detector.detectLang(ti); + if (lang == FONT_LANG::JAPANESE) + family = "Noto Sans CJK JP, Noto Sans JP"; + else if (lang == FONT_LANG::KOREAN) + family = "Noto Sans CJK KR, Noto Sans KR"; + else if (lang == FONT_LANG::CJK) + family = "Noto Sans CJK SC, Noto Sans SC"; + _backend->selectFontFace(family.c_str(), ti.italic, ti.bold); +#else + _backend->selectFontFace(_fontfamily.ptr(), ti.italic, ti.bold); +#endif + _backend->setFontSize(ti.size > 0 ? ti.size : fontGetSize(ti.fontsize)); } #else void RenderContext::fontsSetFont(const TextItem& ti) { - std::lock_guard _lock(_cairo_mutex); - + // Font manager uses cairo directly - only available on desktop + std::lock_guard _lock(_mutex); cairo_font_face_t* _cairo_face = _font_face_manager.selectCairoFontFace(ti); - cairoCheckStatus(); - - cairo_set_font_face(_cr, _cairo_face); - cairoCheckStatus(); - cairo_set_font_size(_cr, ti.size > 0 ? ti.size : fontGetSize(ti.fontsize)); - cairoCheckStatus(); + auto* cairoBackend = dynamic_cast(_backend.get()); + if (cairoBackend) + { + cairo_set_font_face(cairoBackend->getCr(), _cairo_face); + cairo_set_font_size(cairoBackend->getCr(), ti.size > 0 ? ti.size : fontGetSize(ti.fontsize)); + } } #endif -void RenderContext::fontsGetTextExtents(cairo_t* cr, const char* text, int /*size*/, float& dx, float& dy, float& rx, float& ry) +void RenderContext::fontsGetTextExtents(const char* text, int /*size*/, float& dx, float& dy, float& rx, float& ry) { - std::lock_guard _lock(_cairo_mutex); - cairo_text_extents_t te; - cairo_text_extents(cr, text, &te); - cairoCheckStatus(); - - dx = (float)te.width; - dy = (float)te.height; - rx = (float)-te.x_bearing; - ry = (float)-te.y_bearing; + double w, h, x_bearing, y_bearing; + _backend->textExtents(text, w, h, x_bearing, y_bearing); + dx = (float)w; + dy = (float)h; + rx = (float)-x_bearing; + ry = (float)-y_bearing; } float RenderContext::getSpaceWidth() { - // cairo ignores trailing spaces, but takes in account leading spaces in cairo_text_extents, so we need to work around - std::lock_guard _lock(_cairo_mutex); - cairo_text_extents_t te, te_; - cairo_text_extents(_cr, ". .", &te); - cairo_text_extents(_cr, "..", &te_); - cairoCheckStatus(); - return (float)(te.width - te_.width); + double w1, h1, xb1, yb1, w2, h2, xb2, yb2; + _backend->textExtents(". .", w1, h1, xb1, yb1); + _backend->textExtents("..", w2, h2, xb2, yb2); + return (float)(w1 - w2); } void RenderContext::fontsDrawText(const TextItem& ti, const Vec3f& color, bool idle) @@ -171,52 +135,52 @@ void RenderContext::fontsDrawText(const TextItem& ti, const Vec3f& color, bool i */ if (idle) { - cairo_move_to(_cr, ti.bbp.x, ti.bbp.y); - cairo_rectangle(_cr, ti.bbp.x, ti.bbp.y, ti.bbsz.x, ti.bbsz.y); + _backend->moveTo(ti.bbp.x, ti.bbp.y); + _backend->rect(ti.bbp.x, ti.bbp.y, ti.bbsz.x, ti.bbsz.y); bbIncludePath(false); return; } setSingleSource(color); moveTo(ti.bbp); - cairo_matrix_t m; - cairo_get_matrix(_cr, &m); - float scale = (float)m.xx; + RenderMatrix m; + _backend->getMatrix(m.m); + float scale = (float)m.m[0]; double v = scale * (ti.size > 0 ? ti.size : fontGetSize(ti.fontsize)); if (opt.mode != MODE_PDF && opt.mode != MODE_SVG && v < 1.5) { - cairo_rectangle(_cr, ti.bbp.x + ti.bbsz.x / 4, ti.bbp.y + ti.bbsz.y / 4, ti.bbsz.x / 2, ti.bbsz.y / 2); + _backend->rect(ti.bbp.x + ti.bbsz.x / 4, ti.bbp.y + ti.bbsz.y / 4, ti.bbsz.x / 2, ti.bbsz.y / 2); bbIncludePath(false); - cairo_set_line_width(_cr, _settings.unit / 2); - cairo_stroke(_cr); + _backend->setLineWidth(_settings.unit / 2); + _backend->stroke(); return; } moveToRel(ti.relpos); { - std::lock_guard _lock(_cairo_mutex); - cairo_text_path(_cr, ti.text.ptr()); + std::lock_guard _lock(_mutex); + _backend->textPath(ti.text.ptr()); } bbIncludePath(false); - cairo_new_path(_cr); + _backend->beginPath(); moveTo(ti.bbp); moveToRel(ti.relpos); if (metafileFontsToCurves) { // TODO: remove { - std::lock_guard _lock(_cairo_mutex); - cairo_text_path(_cr, ti.text.ptr()); + std::lock_guard _lock(_mutex); + _backend->textPath(ti.text.ptr()); } - cairoCheckStatus(); - cairo_fill(_cr); - cairoCheckStatus(); + backendCheckStatus(); + _backend->fill(); + backendCheckStatus(); } else { - std::lock_guard _lock(_cairo_mutex); - cairo_show_text(_cr, ti.text.ptr()); - cairoCheckStatus(); + std::lock_guard _lock(_mutex); + _backend->showText(ti.text.ptr()); + backendCheckStatus(); } } diff --git a/core/render2d/src/render_item_aux.cpp b/core/render2d/src/render_item_aux.cpp index 06e23b19e7..db3130abbc 100644 --- a/core/render2d/src/render_item_aux.cpp +++ b/core/render2d/src/render_item_aux.cpp @@ -32,7 +32,9 @@ #pragma warning(disable : 4251) #endif +#ifndef __EMSCRIPTEN__ #include +#endif using namespace indigo; @@ -602,6 +604,9 @@ void RenderItemAuxiliary::_drawImage(const EmbeddedImageObject& img) _rc.drawPng(img.getData(), Rect2f(v1, v2)); else if (img.getFormat() == EmbeddedImageObject::EKETSVG) { +#ifdef __EMSCRIPTEN__ + _rc.drawPng(img.getData(), Rect2f(v1, v2)); +#else auto document = lunasvg::Document::loadFromData(img.getData()); if (!document) throw Error("RenderItemAuxiliary::_drawImage: loadFromData error"); @@ -615,6 +620,7 @@ void RenderItemAuxiliary::_drawImage(const EmbeddedImageObject& img) throw Error("RenderItemAuxiliary::_drawImage: writeToPng error"); _rc.drawPng(lunasvgClosure, Rect2f(v1, v2)); +#endif } } diff --git a/third_party/CMakeLists.txt b/third_party/CMakeLists.txt index 8b719bed05..5e4efd49aa 100644 --- a/third_party/CMakeLists.txt +++ b/third_party/CMakeLists.txt @@ -1,7 +1,9 @@ -add_subdirectory(libpng) +if (NOT EMSCRIPTEN) + add_subdirectory(libpng) -option(LUNASVG_BUILD_EXAMPLES OFF) -add_subdirectory(lunasvg) + option(LUNASVG_BUILD_EXAMPLES OFF) + add_subdirectory(lunasvg) +endif() if (BUILD_STANDALONE) # InChI don't have a Conan package yet @@ -12,11 +14,8 @@ if (BUILD_STANDALONE) add_subdirectory(tinyxml2) endif() if (BUILD_INDIGO OR BUILD_INDIGO_UTILS) - if (BUILD_STANDALONE) + if (BUILD_STANDALONE AND NOT EMSCRIPTEN) add_subdirectory(pixman) - if (EMSCRIPTEN) - add_subdirectory(freetype) - endif() add_subdirectory(cairo) endif() endif ()