Skip to content

fix(core)!: prevent holes during SVG generation - #429

Merged
Krasner merged 30 commits into
Ryan-Millard:devfrom
Krasner:dev/core-opt
Jun 19, 2026
Merged

fix(core)!: prevent holes during SVG generation#429
Krasner merged 30 commits into
Ryan-Millard:devfrom
Krasner:dev/core-opt

Conversation

@Krasner

@Krasner Krasner commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

What was changed & why

Current method produces holes when bezier simplification / smoothing is applied since shared contour edges (those between neighboring regions) are processed independently. Here we treat shared edges correctly so that contour simplification does not cause holes.

Work done using Claude Code - trying many ideas to improve upon current implementation.

Let me know if the results look cleaner and better - from my local tests they seem a bit more pleasing.

Fixes: #

Changes

Testing & Verification

Additional Resources

OLD OUTPUT SVG:
cow_old

NEW OUPUT SVG:
cow_new

note the jagged edges in the old svg and holes between edges that should be overlapping

Summary by CodeRabbit

  • New Features

    • Added min_thickness parameter to SVG generation APIs (C, Python, JavaScript, C++) for filtering thin regions
    • Introduced junction detection and preservation—junction points now remain as exact curve endpoints during fitting
    • Enhanced contour construction pipeline with improved shared-edge handling and smoothing near junctions
  • Chores

    • Updated Docker configuration and example applications

@Krasner
Krasner requested a review from Ryan-Millard as a code owner June 8, 2026 15:03
@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a6f1811f-030a-484f-9caa-85b0ce029a28

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

Adds shared-edge contour construction, junction-aware smoothing and curve reduction (Bézier + Douglas–Peucker), refactors Graph::compute_contours to use shared loops by default, and exposes a new min_thickness option through core APIs and language bindings.

Changes

Shared-edge contours with junction-aware smoothing

Layer / File(s) Summary
Curve reduction and Bézier fitting
core/include/internal/bezier.h, core/src/internal/bezier.cpp, core/include/internal/douglas_peucker.h, core/src/internal/douglas_peucker.cpp
Adds a fixed mask overload for fit_curve_reduction to split chains at pinned junctions and fit segments independently; implements dp_curve_reduction (Douglas–Peucker variant) emitting QuadBezier segments.
Junction-aware contour smoothing
core/include/internal/contours.h, core/src/internal/contours.cpp
Introduces coupled_smooth_junctions and updateLockedMasks; increases Savitzky–Golay smoothing radius and preserves junction-locked points during smoothing.
Shared-edge loop construction
core/include/internal/shared_contours.h, core/src/internal/shared_contours.cpp
Implements canonical crack-edge extraction on the corner grid, fits each canonical edge once to Béziers, and assembles per-region directed boundary loops that reuse shared curves.
Graph contour computation and merging
core/include/internal/graph.h, core/src/internal/graph.cpp
Adds analyzeJunctions, refactors Graph::compute_contours() to use build_shared_loops by default with a legacy fallback that uses junction-aware smoothing and fitting; adds distance-transform helpers and thickness-aware merge_small_area_nodes.
Public API and bindings
core/include/img2num.h, core/src/internal/labels_to_svg.cpp, core/src/internal/image_to_svg.cpp, bindings/*, packages/js/*, example-apps/*
Adds ImageToSvgConfig.min_thickness and threads min_thickness through labels_to_svg/image_to_svg, C/JS/Python bindings, WASM wrapper, JS wrappers, and updates examples to pass the new parameter.

Sequence Diagram(s)

sequenceDiagram
  participant GraphCC as Graph::compute_contours
  participant BuildShared as build_shared_loops
  participant SharedEdge as canonical edge extraction
  participant Fit as fit_curve_reduction / dp_curve_reduction
  participant Assemble as per-region loop assembly
  GraphCC->>BuildShared: labels grid, w, h, eps
  BuildShared->>SharedEdge: build crack graph, find junctions
  SharedEdge->>Fit: canonical edge corner chains
  Fit-->>SharedEdge: fitted QuadBezier per canonical edge
  BuildShared->>Assemble: directed region traversals
  Assemble->>Fit: orient/reuse fitted curves
  Assemble-->>GraphCC: label→loop map
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~65 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers

  • Ryan-Millard

Poem

🐰 I hop where junctions hold their place,

shared edges stitched with careful grace,
curves bend true where pins remain,
regions sing — no seams, just gain,
a rabbit cheers this tidy trace.

🚥 Pre-merge checks | ✅ 7 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (7 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
No Ai Slop Pr Description ✅ Passed PR description is specific and non-generic: identifies problem (holes in SVG generation), root cause (independent shared edge processing), and solution. Not AI boilerplate.
No Strangely-Named Root Markdown Files ✅ Passed No unconventional markdown files added at repository root. Only acceptable markdown files (CODE_OF_CONDUCT.md, CONTRIBUTING.md, README.md) exist at root level.
Coderabbit Config Needs Update ✅ Passed PR uses only existing file types (.h, .cpp, .c, .py, .js) all covered by current path_instructions; no new languages, linter configs, or CI files introduced.
Description check ✅ Passed The PR description clearly describes the problem (holes in SVG when bezier simplification is applied to shared contour edges) and the solution (treating shared edges correctly during simplification).
Title check ✅ Passed The title 'fix(core)!: prevent holes during SVG generation' directly describes the main change: addressing holes produced during SVG generation through shared contour edge handling and thickness-based contour merging.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
✨ Simplify code
  • Create PR with simplified code

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions Bot added c/c++ Changes to C or C++ files core labels Jun 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 12

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@core/include/internal/contours.h`:
- Line 52: Add a Doxygen docstring for the function coupled_smooth_junctions
describing that it is a variant of coupled_smooth which additionally locks
junction pixels during smoothing, document the parameter semantics: contours
(vector of contour point vectors), bounds (Rect image bounds), junctions (a
junction mask where nonzero entries mark junction pixels to be
preserved/locked), and width (image width used for raster indexing), and explain
any return/side-effect behavior (modifies contours in-place). Also mention
relationship to coupled_smooth and intended usage (junction locking during
smoothing).

In `@core/include/internal/douglas_peucker.h`:
- Around line 9-26: Convert the existing block comment above dp_curve_reduction
into a Doxygen comment block: add a brief description with `@brief` summarizing
Douglas–Peucker point reduction, then add `@param` tags for each parameter
(chains, fixed, results, eps) and also document the implicit/relevant parameters
described in the text (retract_eps, and the environment overrides IMG2NUM_DP_EPS
and IMG2NUM_DP_RETRACT) and use `@note` or `@details` for behavior about fixed[i][k]
junctions, boundary retraction limits, and that kept points are emitted as
straight-line quads; keep the same content but reformat it to a /** ... */
Doxygen style above the dp_curve_reduction declaration.

In `@core/include/internal/graph.h`:
- Around line 42-47: Add Doxygen-style comments above the two new protected
methods in core/include/internal/graph.h: document getPixel(const
std::vector<uint8_t>& img, int w, int h, int x, int y) to describe parameters,
state that it performs bounds checking and returns 0 for out-of-bounds
coordinates, and explain the returned uint8_t pixel value; document
analyzeJunctions(const std::vector<uint8_t>& skel, int w, int h) to describe
parameters, state that it uses an 8-neighbor crossing-number algorithm to detect
junctions, and explain the return semantics (a vector<uint8_t> mask the same
size as the image where nonzero entries mark junction pixels).

In `@core/include/internal/shared_contours.h`:
- Around line 11-28: The public function build_shared_loops lacks a Doxygen
docstring; replace the plain comments above it with a Doxygen-style block
describing the function, its purpose, parameters, return value, and any
important notes (e.g. coordinate system, treatment of out-of-bounds as exterior,
that edges are canonical and reused), and annotate parameters `@param` labels,
`@param` w, `@param` h, `@param` eps, and `@return` std::unordered_map<int32_t,
std::vector<std::vector<QuadBezier>>> so the API shows up in generated docs;
keep the existing explanatory content but format it using /** ... */ Doxygen
tags and include types/names (QuadBezier, build_shared_loops) for clarity.

In `@core/src/internal/bezier.cpp`:
- Around line 158-186: fit_curve_reduction currently assumes fixed has the same
outer and per-chain sizes as chains; add an upfront validation that fixed.size()
== chains.size() and for each i verify fixed[i].size() == chains[i].size() (or
at least >= chains[i].size()) and handle violations (e.g., throw
std::invalid_argument or assert) before the existing loop so mismatched masks
are caught early and interior junctions aren't silently ignored; reference
function fit_curve_reduction and variables fixed, chains and the loop that uses
fixed[i].size() to locate where to add the checks.

In `@core/src/internal/contours.cpp`:
- Line 694: Add a brief inline comment next to the SavitzkyGolay sg(3, 2)
instantiation explaining why the radius was increased (e.g., to better smooth
junction-aware seam continuity and reduce spurious peaks across junctions),
noting trade-offs (higher cost and potential over-smoothing) and whether this
value is intentionally fixed or should be made configurable (reference
SavitzkyGolay sg(3, 2) and the junction-aware smoothing code path so reviewers
can decide if a runtime/config option is needed).
- Around line 652-662: updateLockedMasks currently computes idx = pt.y * width +
pt.x and indexes junctions without bounds checks; fix by validating pt.x and
pt.y are >= 0, width > 0, compute height = junctions.size() / width (guard
against division by zero and ensure junctions.size() % width == 0 or use
size()/width semantics), ensure pt.x < width and pt.y < height, and verify idx <
junctions.size() before accessing junctions[idx]; also validate locked[c].size()
> p before writing locked[c][p]; if any check fails simply skip that point so
out-of-bounds contour points do not access memory.
- Around line 794-797: Remove the two debug std::cout lines that print "boundary
masks" and "update junctions" surrounding the call to createBoundaryMask;
specifically delete the prints near the creation of lockedMasks (the call to
createBoundaryMask(contours, bounds)) so production code does not emit console
debug output—if runtime diagnostics are required instead, replace with the
project's logging facility rather than std::cout.

In `@core/src/internal/douglas_peucker.cpp`:
- Around line 57-110: The function dp_curve_reduction assumes fixed has the same
outer size as chains and uses fixed[i].size() later; add upfront validation:
check fixed.size() == chains.size() and for each i verify fixed[i].size() ==
chains[i].size() (or at least >= chains[i].size()) and fail fast (e.g.,
return/error/throw or assert/log) if not; perform this validation at the start
of dp_curve_reduction before any processing so mismatched dimensions don't
silently drop interior junctions used by dp_reduce and the bounds/fixed logic.

In `@core/src/internal/graph.cpp`:
- Around line 260-279: compute_contours() was changed to always use the new
shared-edge path (build_shared_loops) but the PR lacks regression tests; add
tests that cover the reported gap artifact and topology-sensitive cases
(donut/holed regions and diagonal-touch junctions) and gate the new behavior
behind a toggle so tests can exercise both implementations. Specifically, add
unit/regression tests that call compute_contours() (or invoke
build_shared_loops() directly) with crafted inputs for: (1) the previously
failing gap artifact, (2) a donut-shaped region, and (3) diagonal-touch
junctions; implement a temporary runtime flag (e.g., enable_shared_edge or
similar) or parameter to compute_contours()/build_shared_loops so tests can run
both the legacy and shared-edge paths and assert identical/topologically-correct
contours before making the shared-edge branch the unconditional default.
- Around line 281-297: The loop repopulates Node::m_contours but overwrites
containment metadata by hard-coding n->m_contours.hierarchy to {-1,-1,-1,-1} and
n->m_contours.is_hole to false; instead, after pushing all
contours/curves/colors for a given node (inside the get_nodes() loop when
handling loops[n->id()]), rebuild the contour hierarchy and hole flags for
n->m_contours before leaving the node: compute each contour's parent by testing
containment (point-in-polygon or bounding-box quick-reject) and set
n->m_contours.hierarchy entries accordingly, then set n->m_contours.is_hole
using nesting parity or contour orientation (or call the project’s existing
contour-hierarchy utility if one exists) so holes and nesting information are
preserved rather than hard-coded.

In `@core/src/internal/shared_contours.cpp`:
- Line 15: Remove the in-band OUTSIDE sentinel and instead represent the canvas
exterior out-of-band: delete the OUTSIDE constant in shared_contours.cpp and
change build_shared_loops() (and its callers) to accept an explicit exterior
marker (e.g. std::optional<int32_t> exterior_id or a separate bool/flag) rather
than treating a numeric label as "outside"; update all checks that compared
labels to OUTSIDE to use the new out-of-band indicator so a real region id equal
to -2147483647 is no longer co-opted and the exterior boundary handling is kept
separate.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 0b7874a5-e114-4078-b09b-63c040b60fbf

📥 Commits

Reviewing files that changed from the base of the PR and between 2427d1d and 7e5ac60.

📒 Files selected for processing (10)
  • core/include/internal/bezier.h
  • core/include/internal/contours.h
  • core/include/internal/douglas_peucker.h
  • core/include/internal/graph.h
  • core/include/internal/shared_contours.h
  • core/src/internal/bezier.cpp
  • core/src/internal/contours.cpp
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Documentation Site / Build Docusaurus Site
  • GitHub Check: Build C/C++ / Build WASM (bindings/js)
  • GitHub Check: Build C/C++ / Build Python
  • GitHub Check: Build C/C++ / Build C & C++
  • GitHub Check: Lint & Validate Code
🧰 Additional context used
📓 Path-based instructions (5)
**/*.{c,cc,cpp,cxx,h,hpp,hxx}

📄 CodeRabbit inference engine (.clang-format)

**/*.{c,cc,cpp,cxx,h,hpp,hxx}: Follow Google style guide for C/C++ code formatting
Use 4 spaces for indentation
Maintain a column limit of 100 characters per line
Do not allow short functions on a single line

Files:

  • core/include/internal/contours.h
  • core/include/internal/bezier.h
  • core/src/internal/bezier.cpp
  • core/include/internal/graph.h
  • core/include/internal/shared_contours.h
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/include/internal/douglas_peucker.h
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
**/*.{hpp,cpp,c,h}

📄 CodeRabbit inference engine (.editorconfig)

**/*.{hpp,cpp,c,h}: Use 4-space indentation for C/C++ files
Maintain 120 character maximum line length for C/C++ files

Files:

  • core/include/internal/contours.h
  • core/include/internal/bezier.h
  • core/src/internal/bezier.cpp
  • core/include/internal/graph.h
  • core/include/internal/shared_contours.h
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/include/internal/douglas_peucker.h
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
core/**/*.{cpp,c,h,hpp}

⚙️ CodeRabbit configuration file

core/**/*.{cpp,c,h,hpp}: This is the Img2Num core C/C++ library. Review for:

  • Memory safety: null pointer dereferences, use-after-free, buffer overflows.
  • Correct RAII usage and smart pointer idioms.
  • Adherence to the .clang-format style; formatting must be applied via
    ./img2num format-cpp (or ./img2num format-wasm for WASM modules),
    NOT by calling clang-format directly.
  • When suggesting build/test steps, always use the Docker-first wrapper scripts
    (./img2num, img2num.ps1, or img2num.bat) rather than direct tool invocations,
    since contributors may not have dependencies installed locally.
  • Must have Doxygen docstrings in the API (img2num.h) files. This is important!

Files:

  • core/include/internal/contours.h
  • core/include/internal/bezier.h
  • core/src/internal/bezier.cpp
  • core/include/internal/graph.h
  • core/include/internal/shared_contours.h
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/include/internal/douglas_peucker.h
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
**/*.{cpp,c,h,hpp}

⚙️ CodeRabbit configuration file

**/*.{cpp,c,h,hpp}: - For any C/C++ code outside core/ (e.g. bindings, example-apps), apply the same
memory-safety and style standards as the core. Formatting via ./img2num format-cpp.

  • Doxygen docstrings are required.

Files:

  • core/include/internal/contours.h
  • core/include/internal/bezier.h
  • core/src/internal/bezier.cpp
  • core/include/internal/graph.h
  • core/include/internal/shared_contours.h
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/include/internal/douglas_peucker.h
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
**

⚙️ CodeRabbit configuration file

**: # Contributing to Img2Num

Want to contribute to Img2Num? There are a few things you need to know.

We wrote a contribution guide to help you get started.

A few important points:

  • Add tests with your PR — new features and bug fixes must include tests where appropriate. PRs without tests are unlikely to be approved.
  • Follow the repository's coding style rules.
  • Use the issue and PR templates when filing issues or submitting code. Your PR will be rejected if you don't.

If you're unsure what to change, open a discussion and someone will assist you.

Questions?

If you have questions or need help:

Thank you for improving Img2Num! 🎨🚀

**: BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100
AllowShortFunctionsOnASingleLine: false

**: root = true

-------------------------

Global defaults

-------------------------

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

-------------------------

C++ / C headers

-------------------------

[*.{hpp,cpp,c,h}]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

-------------------------

CMake

-------------------------

[CMakeLists.txt]
indent_style = space
indent_size = 2
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

[*.cmake]
indent_style = space
indent_size = 2
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

-------------------------

Ja...

Files:

  • core/include/internal/contours.h
  • core/include/internal/bezier.h
  • core/src/internal/bezier.cpp
  • core/include/internal/graph.h
  • core/include/internal/shared_contours.h
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/include/internal/douglas_peucker.h
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
🧠 Learnings (9)
📓 Common learnings
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-04-25T16:20:34.359Z
Learning: In the Ryan-Millard/Img2Num repository, the following follow-up issues were created after merging PR `#307` (Python bindings): `#328` (Python docstrings/docs), `#329` (numpy img.shape inference for width/height), `#330` (.coderabbit.yaml Python tooling config), `#331` (CI workflow for Python bindings build test).
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-04-01T20:45:20.732Z
Learning: In the Ryan-Millard/Img2Num repository, a PR body containing generic boilerplate such as "This contribution was made as part of an open-source documentation improvement initiative. All changes are meant to be helpful additions to the project." is a strong signal of an AI-generated slop PR and should be explicitly flagged during review.
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-05-01T00:15:20.172Z
Learning: In Ryan-Millard/Img2Num, the correct release propagation strategy uses TWO GitHub Actions jobs added to release.yml:
1. `propagate-core`: fires when `core_created == 'true'` → creates separate PRs (not direct commits) for bindings/c, packages/js, and packages/py using `git commit --allow-empty` with conventional commit messages and `gh pr create`. Requires RELEASE_PROPAGATION_TOKEN.
2. `propagate-c`: fires when `c_created == 'true' AND core_created != 'true'` → creates a PR for packages/js only (the guard prevents double-bumping JS when core already propagated to it in the same run).
Dependency graph: core → {bindings/c, packages/js, packages/py}; bindings/c → {packages/js}. packages/py depends directly on core (not via bindings/c). PRs must be created (not direct commits) so Ryan-Millard can review each propagation before merging.
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 339
File: release-please-config.json:3-3
Timestamp: 2026-05-23T23:39:14.889Z
Learning: In Ryan-Millard/Img2Num (release-please-config.json), `separate-pull-requests: true` is intentional and correct. Each package (`core`, `bindings/c`, `packages/js`, `packages/py`) is versioned independently because they have separate logic. A change to `packages/js` or `packages/py` does not imply a change to `core`, so a single combined release PR is not desired. Do not flag `separate-pull-requests: true` as an issue in this repository.
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 339
File: release-please-config.json:3-3
Timestamp: 2026-05-23T23:39:14.889Z
Learning: In Ryan-Millard/Img2Num (release-please-config.json), `separate-pull-requests: true` is intentional and correct. Each package (`core`, `bindings/c`, `packages/js`, `packages/py`) is versioned independently because they each have their own separate package-level logic. A change to `packages/js` or `packages/py` does not imply a change to `core`, so a single combined release PR is explicitly not desired. Do not flag `separate-pull-requests: true` as an issue in this repository.
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-04-23T18:48:02.692Z
Learning: In the Ryan-Millard/Img2Num repository, PR `#311` adds `.coderabbit.yaml` to main. PR `#307` (adding Python .py files) must rebase onto main after `#311` merges so that `.coderabbit.yaml` exists and can be updated with: `ruff: enabled: true`, `pylint: enabled: true`, `flake8: enabled: false` under `tools:`, and a `path_instructions` entry for `**/*.py` covering PEP 8, type annotations, exception handling, resource management, mutable default arguments, and Docker-first command suggestions via the `./img2num` wrapper. The "CodeRabbit Config Needs Update" custom check will automatically flag `#307` if the Python config is missing.
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 335
File: core/src/internal/image_to_svg.cpp:16-19
Timestamp: 2026-05-04T16:50:52.142Z
Learning: In the Img2Num project (Ryan-Millard/Img2Num), the existing Gaussian blur implementation uses zero-padding, which causes a vignette-like darkening artifact at image edges. Because of this, Gaussian blur is not currently a suitable drop-in alternative to bilateral_filter in the image_to_svg pipeline. Any future PR adding a GaussianBlurConfig / SmoothingMethod::GAUSSIAN option must first fix the zero-padding boundary handling (e.g., replicate or reflect padding) before it can be safely exposed as a user-selectable smoothing stage.
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-04-23T18:48:02.712Z
Learning: In the Ryan-Millard/Img2Num repository, PR `#311` adds `.coderabbit.yaml` to main. PR `#307` (adding Python .py files) must rebase onto main after `#311` merges so that `.coderabbit.yaml` exists and can be updated with: `ruff: enabled: true`, `pylint: enabled: true`, `flake8: enabled: false` under `tools:`, and a `path_instructions` entry for `**/*.py` covering PEP 8, type annotations, exception handling, resource management, mutable default arguments, and Docker-first command suggestions via the `./img2num` wrapper. The "CodeRabbit Config Needs Update" custom check will automatically flag `#307` if the Python config is missing.
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 0
File: :0-0
Timestamp: 2026-04-25T16:31:13.204Z
Learning: In the Ryan-Millard/Img2Num repository, issue `#331` (CI workflow for Python bindings build test) was addressed in PR `#307` via the `build-py` job in `.github/workflows/cmake-build.yml`. That job runs `uv venv`, `uv sync --no-build-isolation`, a smoke test (`uv run python3 -c "import img2num;"`), and uploads artifacts.
📚 Learning: 2026-02-25T21:24:34.055Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 272
File: core/include/internal/bilateral_filter_gpu.h:108-109
Timestamp: 2026-02-25T21:24:34.055Z
Learning: In WGSL shaders, .rgb swizzle on a vec4 returns the first three components regardless of color space. Do not assume data is RGB color space when the texture contains non-RGB data (e.g., CIELAB L, A, B). If LAB data is stored, treat and compute distances in LAB space, not RGB. Clearly document shader code paths that rely on specific color spaces and prefer explicit conversions or comments when using swizzled components with non-RGB textures. Apply this guidance to WGSL shader files across the codebase (not just this header) when handling color data or color-like channels.

Applied to files:

  • core/include/internal/contours.h
  • core/include/internal/bezier.h
  • core/include/internal/graph.h
  • core/include/internal/shared_contours.h
  • core/include/internal/douglas_peucker.h
📚 Learning: 2026-04-09T19:05:40.514Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 302
File: core/include/internal/gpu.h:203-216
Timestamp: 2026-04-09T19:05:40.514Z
Learning: In Img2Num’s internal GPU initialization code, if querying adapter limits fails (e.g., `adapter.GetLimits(...)` returns `false`), treat this as a GPU initialization failure and route execution to the CPU fallback path. Do not continue with default limits in this case; the failure should cause the same fallback behavior as other GPU init errors.

Applied to files:

  • core/include/internal/contours.h
  • core/include/internal/bezier.h
  • core/include/internal/graph.h
  • core/include/internal/shared_contours.h
  • core/include/internal/douglas_peucker.h
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral_filter.cpp (src/wasm/modules/image/src/bilateral_filter.cpp), rely on the fact that the center pixel contributes a weight of exactly 1.0 to both spatial and range components (exp(0) = 1) so the normalization sum cannot be zero. This implies you do not need explicit guards against division by zero for normalization in this implementation.

Applied to files:

  • core/src/internal/bezier.cpp
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
📚 Learning: 2026-01-06T04:56:57.269Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/kmeans.cpp:128-134
Timestamp: 2026-01-06T04:56:57.269Z
Learning: In C++ sources compiled for WASM with Emscripten/Clang, designated initializers (e.g., RGBXY{.r = ..., .g = ...}) are allowed as a C++17 extension. When reviewing code that relies on designated initializers, verify that the target toolchain enables CXX_STANDARD 17 or higher and that the build system (CMake/emsdk) uses Emscripten with a compatible clang. If not, avoid such initializers or provide portable alternatives.

Applied to files:

  • core/src/internal/bezier.cpp
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
📚 Learning: 2026-01-06T21:06:24.476Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/cielab.cpp:43-57
Timestamp: 2026-01-06T21:06:24.476Z
Learning: In the Img2Num project, prefer recommending and using the provided docker/script wrappers (e.g., ./img2num format-wasm, ./img2num clean-wasm) instead of invoking local tools directly (e.g., clang-format -i). This reduces dependency requirements for users and ensures consistent tooling across environments. Apply this guidance to C++ source files under the project when reviewing changes.

Applied to files:

  • core/src/internal/bezier.cpp
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
📚 Learning: 2026-01-19T00:02:34.957Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 234
File: src/wasm/modules/image/src/kmeans.cpp:79-81
Timestamp: 2026-01-19T00:02:34.957Z
Learning: In C++ multithreading contexts, prefer unsigned types for thread-count-like parameters (e.g., n_threads) to prevent negative values. Validate that the value is at least 1 before any division to avoid divide-by-zero at runtime. At the start of functions handling such values, create a safe count like: const unsigned int thread_count{std::max(1u, n_threads)}; This ensures non-negative, non-zero usage for divisions and related arithmetic. Apply this pattern to similar parameters across C++ modules, not just the specific file.

Applied to files:

  • core/src/internal/bezier.cpp
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
📚 Learning: 2026-02-25T21:24:19.036Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 272
File: core/src/internal/bilateral_filter_gpu.cpp:171-185
Timestamp: 2026-02-25T21:24:19.036Z
Learning: Adopt brace initialization (e.g., Type var{}) over copy initialization (e.g., Type var = {}) for zero-initialization in C++ across the repository. This should apply to variables, arrays, and structs in most C++ files (e.g., core/src/internal/bilateral_filter_gpu.cpp). It improves safety by avoiding narrowing conversions and makes initialization intent explicit.

Applied to files:

  • core/src/internal/bezier.cpp
  • core/src/internal/shared_contours.cpp
  • core/src/internal/contours.cpp
  • core/src/internal/douglas_peucker.cpp
  • core/src/internal/graph.cpp
📚 Learning: 2026-04-02T18:39:24.330Z
Learnt from: Krasner
Repo: Ryan-Millard/Img2Num PR: 290
File: core/src/internal/graph.cpp:138-208
Timestamp: 2026-04-02T18:39:24.330Z
Learning: In the Ryan-Millard/Img2Num repository, `add_edge_pixel()` on a Node is intentionally designed to produce dual ownership of boundary pixels: a pixel may exist in one node's `m_pixels` AND in a neighboring node's `m_edge_pixels` simultaneously. This dual ownership is required for SVG generation to prevent a 1-pixel boundary gap artifact (the "1 pixel boundary problem"). Edge pixels do not influence node properties like color; they only expand the contour/binary representation for boundary tracing. Do not flag this dual ownership as a bug or misuse of the API.

Applied to files:

  • core/include/internal/graph.h
🪛 Cppcheck (2.20.0)
core/src/internal/shared_contours.cpp

[style] 59-59: The function 'build_shared_loops' is never used.

(unusedFunction)

core/src/internal/contours.cpp

[style] 793-793: The function 'coupled_smooth_junctions' is never used.

(unusedFunction)

core/src/internal/douglas_peucker.cpp

[style] 57-57: The function 'dp_curve_reduction' is never used.

(unusedFunction)

🔇 Additional comments (1)
core/include/internal/bezier.h (1)

6-15: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add Doxygen docstrings for both function overloads.

As per coding guidelines, Doxygen docstrings are required for C/C++ files in core/. Both fit_curve_reduction overloads need proper Doxygen documentation describing their parameters, behavior, and return semantics. The existing comment at lines 9-12 provides good content but should be formatted as a Doxygen block (/** ... */ with @param, @brief tags).

📝 Proposed Doxygen format
+/**
+ * `@brief` Fits quadratic Bezier curves to a set of polyline chains with Douglas-Peucker reduction.
+ *
+ * `@param` chains Input polyline chains (each chain is a sequence of points)
+ * `@param` results Output Bezier curve segments for each chain
+ * `@param` tolerance Maximum deviation tolerance in pixels
+ */
 void fit_curve_reduction(const std::vector<std::vector<Point>> &chains,
                          std::vector<std::vector<QuadBezier>> &results, float tolerance);

+/**
+ * `@brief` Fits quadratic Bezier curves with junction-aware segmentation.
+ *
+ * Chains are split at fixed junction points marked by the fixed mask, so each
+ * junction becomes an exact pinned endpoint. Chains with no fixed points behave
+ * identically to the overload above.
+ *
+ * `@param` chains Input polyline chains
+ * `@param` fixed Junction mask: fixed[i][k] != 0 marks point k of chain i as a junction
+ * `@param` results Output Bezier curve segments for each chain
+ * `@param` tolerance Maximum deviation tolerance in pixels
+ */
-// Same, but `fixed[i][k]!=0` marks point k of chain i as a junction that must NOT
-// move: the chain is split at those points so each becomes an exact (pinned)
-// curve endpoint. Chains with no fixed points fit identically to the overload
-// above.
 void fit_curve_reduction(const std::vector<std::vector<Point>> &chains,
                          const std::vector<std::vector<uint8_t>> &fixed,
                          std::vector<std::vector<QuadBezier>> &results, float tolerance);
			> Likely an incorrect or invalid review comment.

Source: Coding guidelines

Comment thread core/include/internal/contours.h Outdated
Comment thread core/include/internal/douglas_peucker.h Outdated
Comment thread core/include/internal/graph.h
Comment thread core/include/internal/shared_contours.h Outdated
Comment thread core/src/internal/bezier.cpp Outdated
Comment on lines +158 to +186
void fit_curve_reduction(const std::vector<std::vector<Point>> &chains,
const std::vector<std::vector<uint8_t>> &fixed,
std::vector<std::vector<QuadBezier>> &results, float tolerance) {
for (size_t i = 0; i < chains.size(); ++i) {
const std::vector<Point> &chain = chains[i];
const int n = static_cast<int>(chain.size());
std::vector<QuadBezier> result;
if (n < 2) {
results.push_back(result);
continue;
}

// Segment boundaries: chain ends plus every interior junction point.
std::vector<int> bounds;
bounds.push_back(0);
for (int k = 1; k < n - 1; ++k)
if (k < static_cast<int>(fixed[i].size()) && fixed[i][k]) bounds.push_back(k);
bounds.push_back(n - 1);

// Fit each [bounds[s], bounds[s+1]] piece; consecutive pieces share the
// junction point, so the curve stays continuous and pinned there.
for (size_t s = 0; s + 1 < bounds.size(); ++s) {
const int a = bounds[s], b = bounds[s + 1];
std::vector<Point> seg(chain.begin() + a, chain.begin() + b + 1);
fitRecursive(seg, tolerance, result);
}
results.push_back(result);
}
}

@coderabbitai coderabbitai Bot Jun 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate that fixed dimensions match chains dimensions.

The function assumes fixed[i].size() is compatible with chains[i].size(), but only checks the size at line 174 within the loop. If a caller provides a fixed mask with mismatched dimensions (e.g., fixed[i].size() < chains[i].size()), interior junctions beyond the fixed mask length will be silently ignored. Consider adding an upfront validation that fixed.size() == chains.size() and optionally that each fixed[i].size() == chains[i].size() to catch contract violations early.

🛡️ Proposed validation
 void fit_curve_reduction(const std::vector<std::vector<Point>> &chains,
                          const std::vector<std::vector<uint8_t>> &fixed,
                          std::vector<std::vector<QuadBezier>> &results, float tolerance) {
+    if (fixed.size() != chains.size()) {
+        throw std::invalid_argument("fixed and chains must have the same outer dimension");
+    }
     for (size_t i = 0; i < chains.size(); ++i) {
         const std::vector<Point> &chain = chains[i];
         const int n = static_cast<int>(chain.size());
+        if (i < fixed.size() && fixed[i].size() != static_cast<size_t>(n)) {
+            throw std::invalid_argument("fixed[i] and chains[i] must have the same size");
+        }
         std::vector<QuadBezier> result;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/bezier.cpp` around lines 158 - 186, fit_curve_reduction
currently assumes fixed has the same outer and per-chain sizes as chains; add an
upfront validation that fixed.size() == chains.size() and for each i verify
fixed[i].size() == chains[i].size() (or at least >= chains[i].size()) and handle
violations (e.g., throw std::invalid_argument or assert) before the existing
loop so mismatched masks are caught early and interior junctions aren't silently
ignored; reference function fit_curve_reduction and variables fixed, chains and
the loop that uses fixed[i].size() to locate where to add the checks.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Krasner please check this out. I'm not sure if this is even likely.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread core/src/internal/contours.cpp Outdated
Comment thread core/src/internal/douglas_peucker.cpp Outdated
Comment thread core/src/internal/graph.cpp Outdated
Comment thread core/src/internal/graph.cpp Outdated
Comment on lines +281 to +297
for (const Node_ptr &n : get_nodes()) {
if (n->area() == 0) continue;
n->clear_contour();
auto it = loops.find(n->id());
if (it == loops.end()) continue;
ImageLib::RGBPixel<uint8_t> c = n->color();
ImageLib::RGBAPixel<uint8_t> col{c.red, c.green, c.blue, 255};
for (std::vector<QuadBezier> &curve : it->second) {
std::vector<Point> anchors; // keep contours[] parallel to curves[]
anchors.reserve(curve.size() + 1);
for (const QuadBezier &q : curve) anchors.push_back(q.p0);
if (!curve.empty()) anchors.push_back(curve.back().p2);
n->m_contours.contours.push_back(std::move(anchors));
n->m_contours.curves.push_back(std::move(curve));
n->m_contours.colors.push_back(col);
n->m_contours.hierarchy.push_back({-1, -1, -1, -1});
n->m_contours.is_hole.push_back(false);

@coderabbitai coderabbitai Bot Jun 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Shared-loop mode drops hole semantics from m_contours.

This repopulates Node::m_contours with every returned ring, but it hard-codes hierarchy to {-1, -1, -1, -1} and is_hole to false for all of them. That breaks the existing contour contract for regions with interior voids or nested islands: downstream consumers still receive multiple loops, but no longer know which ones are holes or how they nest. Please rebuild hole/containment metadata before storing the loops.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/graph.cpp` around lines 281 - 297, The loop repopulates
Node::m_contours but overwrites containment metadata by hard-coding
n->m_contours.hierarchy to {-1,-1,-1,-1} and n->m_contours.is_hole to false;
instead, after pushing all contours/curves/colors for a given node (inside the
get_nodes() loop when handling loops[n->id()]), rebuild the contour hierarchy
and hole flags for n->m_contours before leaving the node: compute each contour's
parent by testing containment (point-in-polygon or bounding-box quick-reject)
and set n->m_contours.hierarchy entries accordingly, then set
n->m_contours.is_hole using nesting parity or contour orientation (or call the
project’s existing contour-hierarchy utility if one exists) so holes and nesting
information are preserved rather than hard-coded.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Krasner please check this out. I understand that we don't really use the hierarchical information because we're more focused on the actual edges, but we may want to preserve this in the future.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Comment thread core/src/internal/shared_contours.cpp Outdated
@Krasner Krasner changed the title core: Update SVG generating step to prevent holes fix(core): Update SVG generating step to prevent holes Jun 8, 2026
@Ryan-Millard

Copy link
Copy Markdown
Owner

Hi. This sounds good, but I still haven't gotten my PC properly fixed, so I can't review it easily.🥲

I'll try my best when I get the chance. Thank you!

@Ryan-Millard Ryan-Millard changed the title fix(core): Update SVG generating step to prevent holes fix(core): prevent holes during SVG generation Jun 8, 2026
@Krasner

Krasner commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator Author

Hi. This sounds good, but I still haven't gotten my PC properly fixed, so I can't review it easily.🥲

I'll try my best when I get the chance. Thank you!

Oh right, I forgot... Hope you get your new PC soon!

… prevent many very thin neighbors next to each other
@github-actions github-actions Bot added js/ts example-app Updates to code related to demonstration (example) applications labels Jun 8, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (5)
core/src/internal/shared_contours.cpp (2)

1-57: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add Doxygen documentation for all functions in this core file.

As per coding guidelines, Doxygen docstrings are required for C/C++ files in core/. Document each function:

  • smooth_edge: Explain the border-preserving smoothing algorithm, parameters, and the iterative averaging approach
  • fit_edge: Document that it smooths then fits to quadratic Béziers, explain the eps parameter's role
  • reverse_curve: Describe the in-place reversal of Bézier curve direction
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/shared_contours.cpp` around lines 1 - 57, Add Doxygen-style
comments for each internal function in this file: document smooth_edge to
describe it performs endpoint- and border-preserving smoothing by iterative
weighted averaging, list parameters (std::vector<Point> &p, int w, int h, int
iters), explain that points on the image frame are locked and how many
iterations control smoothing; document fit_edge to state it copies and smooths
the corner polyline, then fits quadratic Bézier segments via
fit_curve_reduction, describe parameters (const std::vector<Point> &corners, int
w, int h, float eps) and that eps controls fitting tolerance and that the
function returns a vector<QuadBezier> (possibly empty); document reverse_curve
to state it reverses the order of QuadBezier segments in-place and swaps p0/p2
on each segment to flip direction (parameter: std::vector<QuadBezier> &c).
Include brief `@param` and `@return` tags and mark these comments above the
corresponding function definitions (smooth_edge, fit_edge, reverse_curve).

Source: Coding guidelines


59-65: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate that eps is positive.

The eps parameter controls curve-fitting tolerance but is never validated. Negative or zero values could cause unexpected behavior in fit_curve_reduction. Add a guard at the function entry:

🛡️ Proposed validation
 std::unordered_map<int32_t, std::vector<std::vector<QuadBezier>>> build_shared_loops(
     const std::vector<int32_t> &labels, int w, int h, float eps) {
+    if (eps <= 0.0f) {
+        eps = 1.0f;  // or throw, depending on error-handling policy
+    }
     const int W1 = w + 1;  // corner grid width
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/shared_contours.cpp` around lines 59 - 65, In
build_shared_loops, validate the eps parameter at function entry (e.g., if eps
<= 0.0f) and guard against non-positive values by returning an error or throwing
a std::invalid_argument (or using an assert) before any calls to
fit_curve_reduction; reference the build_shared_loops function and the eps
parameter so the check occurs immediately after the function signature and
prevents downstream misuse in fit_curve_reduction.
core/src/internal/graph.cpp (3)

212-258: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add Doxygen docstrings for analyzeJunctions.

As per coding guidelines, internal/API functions in the core library must have Doxygen docstrings explaining purpose, parameters, and return values.

📝 Suggested docstring
+/**
+ * `@brief` Classify skeleton pixels as junctions based on 8-neighbor transitions.
+ * `@param` skel Binary skeleton mask (1 = skeleton pixel, 0 = background).
+ * `@param` w Width of the mask.
+ * `@param` h Height of the mask.
+ * `@return` A mask where 1 indicates a junction pixel (3+ branches).
+ */
 std::vector<uint8_t> Graph::analyzeJunctions(const std::vector<uint8_t>& skel, int w, int h) {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/graph.cpp` around lines 212 - 258, Add a Doxygen-style
comment block above the function Graph::analyzeJunctions describing its purpose,
parameters, and return value: explain that it analyzes a binary skeleton image
(parameter skel) of width w and height h, that it computes junction (and
optionally endpoint) pixels using 8-neighbor crossing-number logic, list the
parameters (const std::vector<uint8_t>& skel, int w, int h) and state the return
type (std::vector<uint8_t> junction_map where 1 marks junction pixels and 0
marks non-junctions), and note any assumptions (row-major layout, pixel values
nonzero = foreground). Ensure the comment uses Doxygen tags (`@brief`, `@param`,
`@return`) and is placed immediately above the analyzeJunctions definition.

Source: Coding guidelines


262-262: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

if (true) makes the legacy path dead code.

The if (true) on line 262 unconditionally takes the shared-edge branch, making the entire else block (lines 302-384) unreachable dead code. Either remove the dead code or use a proper compile-time or runtime flag to allow selection between implementations during testing/rollout.

🔧 Suggested approach

If you intend to keep the legacy path for testing/comparison, consider a runtime or compile-time toggle:

-    if (true) {
+    constexpr bool USE_SHARED_EDGE_CONTOURS = true;  // TODO: remove legacy path after validation
+    if (USE_SHARED_EDGE_CONTOURS) {

Or remove the dead code entirely if the legacy path is no longer needed.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/graph.cpp` at line 262, The unconditional "if (true)"
branch in the shared-edge selection logic makes the legacy else block
unreachable; either remove the legacy branch entirely (delete the else block and
tests referencing it) or replace "if (true)" with a proper toggle (e.g., a
compile-time macro or a runtime/config flag) so callers can choose between the
shared-edge implementation and the legacy implementation during testing; update
the surrounding function (the conditional guarding the shared-edge vs legacy
path) to read that flag and add a short comment explaining the toggle's purpose.

343-343: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove debug std::cout statement from production code.

Line 343 contains a debug print statement that should not remain in production code.

🗑️ Suggested fix
-        std::cout << "Apply smoothing" << std::endl;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/graph.cpp` at line 343, Remove the debug print statement
`std::cout << "Apply smoothing" << std::endl;` from production code in
core/src/internal/graph.cpp; locate the occurrence (inside the graph smoothing
routine) and delete that line, or replace it with the project's logging facility
if a persistent informational message is required (use the existing logger call
pattern used elsewhere in the file/class).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@bindings/c/include/cimg2num.h`:
- Line 45: Add a documentation comment describing the purpose and units/range of
the min_thickness field in the struct where min_thickness is declared (in
cimg2num.h) so it matches the style used for other fields in the header; update
the comment format to mirror neighboring fields (e.g., brief one-line // or /**
*/ comment) and include that it represents the minimum stroke/line thickness
used by the algorithm (units/pixel integer, expected range or special values if
any).

In `@bindings/py/src/img2num_pybind.cpp`:
- Around line 116-126: The pybind wrapper for labels_to_svg currently requires
min_thickness as a positional argument, breaking existing callers; update the
pybind binding for the lambda that calls img2num::labels_to_svg so the
pybind11::arg("min_thickness") has a default value (e.g., = 10 to match the JS
binding or = 0 to disable), leaving the lambda signature and call unchanged but
changing the argument declaration to include that default and updating the final
docstring if needed.

In `@core/include/img2num.h`:
- Line 44: Add a Doxygen comment for the ImageToSvgConfig field min_thickness
describing what it controls, its units and typical valid range (e.g., "minimum
stroke thickness in pixels" and whether zero disables the check), matching the
style of nearby fields' docstrings; update the declaration of int min_thickness
= 0; in ImageToSvgConfig with a brief one-line /** ... */ above it that explains
purpose, units, default value, and any edge-case behavior.

In `@core/src/internal/graph.cpp`:
- Around line 387-470: Add brief Doxygen-style docstrings above the
anonymous-namespace helper functions dt_1d and max_inscribed_radius describing
their purpose, inputs, outputs, and any important invariants: for dt_1d explain
it implements the Felzenszwalb & Huttenlocher 1D squared-distance transform,
describe parameters (const std::vector<float> &f, std::vector<float> &d, int n)
and that d is filled with min_p((q-p)^2+f[p]); for max_inscribed_radius describe
it computes the largest inscribed-disk radius (in pixels) for a Node_ptr region,
note that it uses a padded binary mask, performs separable 2-pass transforms
calling dt_1d, and returns the radius (sqrt of max squared distance). Ensure the
comments are Doxygen formatted (brief summary, `@param`, `@return`) and placed
immediately above each function definition.
- Line 270: The local variable eps uses copy initialization; update its
declaration in graph.cpp (variable eps) to use brace initialization per repo
convention by declaring eps with braces around its initializer (retain the 0.25f
value) instead of using the equals-style initialization.

In `@core/src/internal/shared_contours.cpp`:
- Line 44: Replace the magic literal 5 passed to smooth_edge(pts, w, h, 5) with
a named constant or parameter: introduce a constexpr int (e.g.,
SMOOTHING_ITERATIONS = 5) near the top of the translation unit or expose an
argument on the public API and use that symbol in the call site; update any
function signature or callers as needed so smooth_edge(...) uses the named
constant or forwarded parameter instead of the hardcoded 5, keeping the
parameter name descriptive (e.g., smoothingIterations) and preserving current
behavior by defaulting to 5 if you add a new parameter.

In `@example-apps/console-c/main.c`:
- Line 74: Add an inline comment next to the call to img2num_labels_to_svg
explaining the sixth parameter (min_thickness): state that min_thickness is the
minimum stroke/segment thickness in pixels to include in the SVG (use 0 to
disable thickness filtering), suggest typical values (e.g. 1–3 for thin fonts)
and note its effect (filters out tiny noisy components). Update the comment near
the img2num_labels_to_svg(...) invocation to briefly describe this behavior and
recommended use.

In `@example-apps/console-cpp/main.cpp`:
- Around line 69-72: Add an inline comment above or next to the new
config.min_thickness assignment explaining what ImageToSvgConfig::min_thickness
controls, its units/range, default behavior when unset, and how it affects
image_to_svg output (e.g., it prevents stroke/path widths below this pixel
threshold from being emitted or merged). Update the example in main.cpp so the
comment sits beside config.min_thickness (reference symbols: ImageToSvgConfig,
config, min_thickness, image_to_svg) and keep the comment short and user-facing.
- Around line 67-72: res_svg is computed by calling img2num::labels_to_svg but
never used; either remove the unused call (delete the std::string res_svg{...}
line) or explicitly demonstrate both APIs by keeping the labels_to_svg call,
renaming its result (e.g., res_labels_svg), and writing it to a separate file
alongside res_svg2 created by img2num::image_to_svg; update identifiers res_svg
/ labels_to_svg / res_svg2 / image_to_svg and ensure img_data, out_labels,
width, height are passed correctly when writing both outputs.

In `@example-apps/console-py/main.py`:
- Line 26: The k parameter was increased to 64 for img2num.kmeans (and in
ImageToSvgConfig(kmeans={"k": 64})) to address the new shared-edge contour/holes
fix; add a concise inline comment next to the img2num.kmeans call and the
ImageToSvgConfig instantiation explaining that the higher k produces finer
clusters which helps avoid merged/shared-edge contours and prevents spurious
holes after the contour/hole-fix, and mention any trade-offs (slower runtime,
more SVG complexity); alternatively, add the same short note to the
example-apps/console-py README if you prefer centralized documentation.
- Line 29: Call to labels_to_svg is missing required width, height, min_area and
min_thickness arguments; update the call to pass img.shape[1] as width,
img.shape[0] as height, min_area as 100 and min_thickness as 0 so replace the
single-arg call to img2num.labels_to_svg(img, labels, 100) with a six-argument
call using labels_to_svg(img, labels, img.shape[1], img.shape[0], 100, 0).

---

Outside diff comments:
In `@core/src/internal/graph.cpp`:
- Around line 212-258: Add a Doxygen-style comment block above the function
Graph::analyzeJunctions describing its purpose, parameters, and return value:
explain that it analyzes a binary skeleton image (parameter skel) of width w and
height h, that it computes junction (and optionally endpoint) pixels using
8-neighbor crossing-number logic, list the parameters (const
std::vector<uint8_t>& skel, int w, int h) and state the return type
(std::vector<uint8_t> junction_map where 1 marks junction pixels and 0 marks
non-junctions), and note any assumptions (row-major layout, pixel values nonzero
= foreground). Ensure the comment uses Doxygen tags (`@brief`, `@param`, `@return`)
and is placed immediately above the analyzeJunctions definition.
- Line 262: The unconditional "if (true)" branch in the shared-edge selection
logic makes the legacy else block unreachable; either remove the legacy branch
entirely (delete the else block and tests referencing it) or replace "if (true)"
with a proper toggle (e.g., a compile-time macro or a runtime/config flag) so
callers can choose between the shared-edge implementation and the legacy
implementation during testing; update the surrounding function (the conditional
guarding the shared-edge vs legacy path) to read that flag and add a short
comment explaining the toggle's purpose.
- Line 343: Remove the debug print statement `std::cout << "Apply smoothing" <<
std::endl;` from production code in core/src/internal/graph.cpp; locate the
occurrence (inside the graph smoothing routine) and delete that line, or replace
it with the project's logging facility if a persistent informational message is
required (use the existing logger call pattern used elsewhere in the
file/class).

In `@core/src/internal/shared_contours.cpp`:
- Around line 1-57: Add Doxygen-style comments for each internal function in
this file: document smooth_edge to describe it performs endpoint- and
border-preserving smoothing by iterative weighted averaging, list parameters
(std::vector<Point> &p, int w, int h, int iters), explain that points on the
image frame are locked and how many iterations control smoothing; document
fit_edge to state it copies and smooths the corner polyline, then fits quadratic
Bézier segments via fit_curve_reduction, describe parameters (const
std::vector<Point> &corners, int w, int h, float eps) and that eps controls
fitting tolerance and that the function returns a vector<QuadBezier> (possibly
empty); document reverse_curve to state it reverses the order of QuadBezier
segments in-place and swaps p0/p2 on each segment to flip direction (parameter:
std::vector<QuadBezier> &c). Include brief `@param` and `@return` tags and mark
these comments above the corresponding function definitions (smooth_edge,
fit_edge, reverse_curve).
- Around line 59-65: In build_shared_loops, validate the eps parameter at
function entry (e.g., if eps <= 0.0f) and guard against non-positive values by
returning an error or throwing a std::invalid_argument (or using an assert)
before any calls to fit_curve_reduction; reference the build_shared_loops
function and the eps parameter so the check occurs immediately after the
function signature and prevents downstream misuse in fit_curve_reduction.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a90b7b18-6405-4b59-bd0c-3ec97350c238

📥 Commits

Reviewing files that changed from the base of the PR and between 7e5ac60 and 37309f0.

📒 Files selected for processing (14)
  • bindings/c/include/cimg2num.h
  • bindings/c/src/cimg2num.cpp
  • bindings/js/src/wasm_wrapper.c
  • bindings/py/src/img2num_pybind.cpp
  • core/include/img2num.h
  • core/include/internal/graph.h
  • core/src/internal/graph.cpp
  • core/src/internal/image_to_svg.cpp
  • core/src/internal/labels_to_svg.cpp
  • core/src/internal/shared_contours.cpp
  • example-apps/console-c/main.c
  • example-apps/console-cpp/main.cpp
  • example-apps/console-py/main.py
  • packages/js/safeWasmWrappers.js
📜 Review details
⏰ Context from checks skipped due to timeout of 120000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (5)
  • GitHub Check: Build Documentation Site / Build Docusaurus Site
  • GitHub Check: Build C/C++ / Build WASM (bindings/js)
  • GitHub Check: Build C/C++ / Build C & C++
  • GitHub Check: Build C/C++ / Build Python
  • GitHub Check: Lint & Validate Code
🧰 Additional context used
📓 Path-based instructions (9)
**/*.{c,cc,cpp,cxx,h,hpp,hxx}

📄 CodeRabbit inference engine (.clang-format)

**/*.{c,cc,cpp,cxx,h,hpp,hxx}: Follow Google style guide for C/C++ code formatting
Use 4 spaces for indentation
Maintain a column limit of 100 characters per line
Do not allow short functions on a single line

Files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-c/main.c
  • bindings/c/include/cimg2num.h
  • example-apps/console-cpp/main.cpp
  • core/include/img2num.h
  • bindings/c/src/cimg2num.cpp
  • bindings/js/src/wasm_wrapper.c
  • core/include/internal/graph.h
  • core/src/internal/labels_to_svg.cpp
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
**/*.{hpp,cpp,c,h}

📄 CodeRabbit inference engine (.editorconfig)

**/*.{hpp,cpp,c,h}: Use 4-space indentation for C/C++ files
Maintain 120 character maximum line length for C/C++ files

Files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-c/main.c
  • bindings/c/include/cimg2num.h
  • example-apps/console-cpp/main.cpp
  • core/include/img2num.h
  • bindings/c/src/cimg2num.cpp
  • bindings/js/src/wasm_wrapper.c
  • core/include/internal/graph.h
  • core/src/internal/labels_to_svg.cpp
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
core/**/*.{cpp,c,h,hpp}

⚙️ CodeRabbit configuration file

core/**/*.{cpp,c,h,hpp}: This is the Img2Num core C/C++ library. Review for:

  • Memory safety: null pointer dereferences, use-after-free, buffer overflows.
  • Correct RAII usage and smart pointer idioms.
  • Adherence to the .clang-format style; formatting must be applied via
    ./img2num format-cpp (or ./img2num format-wasm for WASM modules),
    NOT by calling clang-format directly.
  • When suggesting build/test steps, always use the Docker-first wrapper scripts
    (./img2num, img2num.ps1, or img2num.bat) rather than direct tool invocations,
    since contributors may not have dependencies installed locally.
  • Must have Doxygen docstrings in the API (img2num.h) files. This is important!

Files:

  • core/src/internal/image_to_svg.cpp
  • core/include/img2num.h
  • core/include/internal/graph.h
  • core/src/internal/labels_to_svg.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
**/*.{cpp,c,h,hpp}

⚙️ CodeRabbit configuration file

**/*.{cpp,c,h,hpp}: - For any C/C++ code outside core/ (e.g. bindings, example-apps), apply the same
memory-safety and style standards as the core. Formatting via ./img2num format-cpp.

  • Doxygen docstrings are required.

Files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-c/main.c
  • bindings/c/include/cimg2num.h
  • example-apps/console-cpp/main.cpp
  • core/include/img2num.h
  • bindings/c/src/cimg2num.cpp
  • bindings/js/src/wasm_wrapper.c
  • core/include/internal/graph.h
  • core/src/internal/labels_to_svg.cpp
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
**

⚙️ CodeRabbit configuration file

**: # Contributing to Img2Num

Want to contribute to Img2Num? There are a few things you need to know.

We wrote a contribution guide to help you get started.

A few important points:

  • Add tests with your PR — new features and bug fixes must include tests where appropriate. PRs without tests are unlikely to be approved.
  • Follow the repository's coding style rules.
  • Use the issue and PR templates when filing issues or submitting code. Your PR will be rejected if you don't.

If you're unsure what to change, open a discussion and someone will assist you.

Questions?

If you have questions or need help:

Thank you for improving Img2Num! 🎨🚀

**: BasedOnStyle: Google
IndentWidth: 4
ColumnLimit: 100
AllowShortFunctionsOnASingleLine: false

**: root = true

-------------------------

Global defaults

-------------------------

[*]
charset = utf-8
end_of_line = lf
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

-------------------------

C++ / C headers

-------------------------

[*.{hpp,cpp,c,h}]
indent_style = space
indent_size = 4
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

-------------------------

CMake

-------------------------

[CMakeLists.txt]
indent_style = space
indent_size = 2
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

[*.cmake]
indent_style = space
indent_size = 2
charset = utf-8
end_of_line = lf
trim_trailing_whitespace = true
insert_final_newline = true
max_line_length = 120

-------------------------

Ja...

Files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-py/main.py
  • example-apps/console-c/main.c
  • bindings/c/include/cimg2num.h
  • example-apps/console-cpp/main.cpp
  • core/include/img2num.h
  • bindings/c/src/cimg2num.cpp
  • bindings/js/src/wasm_wrapper.c
  • core/include/internal/graph.h
  • core/src/internal/labels_to_svg.cpp
  • packages/js/safeWasmWrappers.js
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
example-apps/**

⚙️ CodeRabbit configuration file

example-apps/**: - Example applications. These are for demonstration; keep them minimal and ensure
they correctly reflect the public API. Flag any use of internal/private APIs.

  • IMPORTANT: These example apps must be a good reflection of how to use Img2Num's
    library, which means that they need good comments and must have clean code. This
    is meant to be external and is designed for others to use to get started.

Files:

  • example-apps/console-py/main.py
  • example-apps/console-c/main.c
  • example-apps/console-cpp/main.cpp
**/*.py

⚙️ CodeRabbit configuration file

**/*.py: Python source files. Review for:

  • PEP 8 compliance (enforced by ruff; focus on logic, not style).
  • Type annotation coverage — prefer typed signatures for public functions.
  • Exception handling: avoid bare except: clauses; always catch specific exceptions.
  • Resource management: use with statements for file/socket/connection handling.
  • No mutable default arguments (e.g. def foo(x=[]):).
  • When suggesting how to run Python tooling, prefer the ./img2num Docker wrapper
    rather than direct python / ruff / pylint invocations.

Files:

  • example-apps/console-py/main.py
bindings/**

⚙️ CodeRabbit configuration file

bindings/**: Language bindings for the Img2Num library. Ensure the public API surface matches
the core C/C++ headers and that error propagation is handled correctly for each
binding language. Also ensure the relevant docstrings are present to enable
auto-generation of documentation.

Files:

  • bindings/c/include/cimg2num.h
  • bindings/c/src/cimg2num.cpp
  • bindings/js/src/wasm_wrapper.c
  • bindings/py/src/img2num_pybind.cpp
**/*.{js,ts,jsx,tsx}

📄 CodeRabbit inference engine (.editorconfig)

**/*.{js,ts,jsx,tsx}: Use 2-space indentation for JavaScript and TypeScript files
Maintain 200 character maximum line length for JavaScript/TypeScript files

Files:

  • packages/js/safeWasmWrappers.js
🧠 Learnings (8)
📚 Learning: 2025-12-31T17:46:54.476Z
Learnt from: fransafu
Repo: Ryan-Millard/Img2Num PR: 176
File: src/wasm/modules/image/src/bilateral_filter.cpp:98-101
Timestamp: 2025-12-31T17:46:54.476Z
Learning: In bilateral_filter.cpp (src/wasm/modules/image/src/bilateral_filter.cpp), rely on the fact that the center pixel contributes a weight of exactly 1.0 to both spatial and range components (exp(0) = 1) so the normalization sum cannot be zero. This implies you do not need explicit guards against division by zero for normalization in this implementation.

Applied to files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-cpp/main.cpp
  • bindings/c/src/cimg2num.cpp
  • core/src/internal/labels_to_svg.cpp
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
📚 Learning: 2026-01-06T04:56:57.269Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/kmeans.cpp:128-134
Timestamp: 2026-01-06T04:56:57.269Z
Learning: In C++ sources compiled for WASM with Emscripten/Clang, designated initializers (e.g., RGBXY{.r = ..., .g = ...}) are allowed as a C++17 extension. When reviewing code that relies on designated initializers, verify that the target toolchain enables CXX_STANDARD 17 or higher and that the build system (CMake/emsdk) uses Emscripten with a compatible clang. If not, avoid such initializers or provide portable alternatives.

Applied to files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-cpp/main.cpp
  • bindings/c/src/cimg2num.cpp
  • core/src/internal/labels_to_svg.cpp
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
📚 Learning: 2026-01-06T21:06:24.476Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 191
File: src/wasm/modules/image/src/cielab.cpp:43-57
Timestamp: 2026-01-06T21:06:24.476Z
Learning: In the Img2Num project, prefer recommending and using the provided docker/script wrappers (e.g., ./img2num format-wasm, ./img2num clean-wasm) instead of invoking local tools directly (e.g., clang-format -i). This reduces dependency requirements for users and ensures consistent tooling across environments. Apply this guidance to C++ source files under the project when reviewing changes.

Applied to files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-cpp/main.cpp
  • bindings/c/src/cimg2num.cpp
  • core/src/internal/labels_to_svg.cpp
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
📚 Learning: 2026-01-19T00:02:34.957Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 234
File: src/wasm/modules/image/src/kmeans.cpp:79-81
Timestamp: 2026-01-19T00:02:34.957Z
Learning: In C++ multithreading contexts, prefer unsigned types for thread-count-like parameters (e.g., n_threads) to prevent negative values. Validate that the value is at least 1 before any division to avoid divide-by-zero at runtime. At the start of functions handling such values, create a safe count like: const unsigned int thread_count{std::max(1u, n_threads)}; This ensures non-negative, non-zero usage for divisions and related arithmetic. Apply this pattern to similar parameters across C++ modules, not just the specific file.

Applied to files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-cpp/main.cpp
  • bindings/c/src/cimg2num.cpp
  • core/src/internal/labels_to_svg.cpp
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
📚 Learning: 2026-02-25T21:24:19.036Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 272
File: core/src/internal/bilateral_filter_gpu.cpp:171-185
Timestamp: 2026-02-25T21:24:19.036Z
Learning: Adopt brace initialization (e.g., Type var{}) over copy initialization (e.g., Type var = {}) for zero-initialization in C++ across the repository. This should apply to variables, arrays, and structs in most C++ files (e.g., core/src/internal/bilateral_filter_gpu.cpp). It improves safety by avoiding narrowing conversions and makes initialization intent explicit.

Applied to files:

  • core/src/internal/image_to_svg.cpp
  • example-apps/console-cpp/main.cpp
  • bindings/c/src/cimg2num.cpp
  • core/src/internal/labels_to_svg.cpp
  • bindings/py/src/img2num_pybind.cpp
  • core/src/internal/graph.cpp
  • core/src/internal/shared_contours.cpp
📚 Learning: 2026-02-25T21:24:34.055Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 272
File: core/include/internal/bilateral_filter_gpu.h:108-109
Timestamp: 2026-02-25T21:24:34.055Z
Learning: In WGSL shaders, .rgb swizzle on a vec4 returns the first three components regardless of color space. Do not assume data is RGB color space when the texture contains non-RGB data (e.g., CIELAB L, A, B). If LAB data is stored, treat and compute distances in LAB space, not RGB. Clearly document shader code paths that rely on specific color spaces and prefer explicit conversions or comments when using swizzled components with non-RGB textures. Apply this guidance to WGSL shader files across the codebase (not just this header) when handling color data or color-like channels.

Applied to files:

  • bindings/c/include/cimg2num.h
  • core/include/img2num.h
  • core/include/internal/graph.h
📚 Learning: 2026-04-27T15:40:33.329Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 333
File: bindings/c/src/cimg2num.cpp:0-0
Timestamp: 2026-04-27T15:40:33.329Z
Learning: In the Ryan-Millard/Img2Num C bindings (e.g., `bindings/c/src/cimg2num.cpp`) that expose `extern "C"` functions, do not allow C++ exceptions (including `std::bad_alloc` and exceptions thrown from lambdas used internally) to propagate out of these C binding functions. Use the C-API failure idiom instead: catch exceptions internally as needed, clear/record error state via the existing error mechanism, and return `nullptr` (or the documented sentinel return value) on failure so the C caller can detect errors by checking the return value.

Applied to files:

  • bindings/c/src/cimg2num.cpp
📚 Learning: 2026-04-09T19:05:40.514Z
Learnt from: Ryan-Millard
Repo: Ryan-Millard/Img2Num PR: 302
File: core/include/internal/gpu.h:203-216
Timestamp: 2026-04-09T19:05:40.514Z
Learning: In Img2Num’s internal GPU initialization code, if querying adapter limits fails (e.g., `adapter.GetLimits(...)` returns `false`), treat this as a GPU initialization failure and route execution to the CPU fallback path. Do not continue with default limits in this case; the failure should cause the same fallback behavior as other GPU init errors.

Applied to files:

  • core/include/internal/graph.h
🪛 Cppcheck (2.20.0)
bindings/js/src/wasm_wrapper.c

[style] 40-40: The function 'image_to_svg' is never used.

(unusedFunction)

🔇 Additional comments (15)
core/src/internal/shared_contours.cpp (1)

15-15: The OUTSIDE sentinel issue has already been flagged in previous review comments.

core/include/internal/graph.h (2)

42-47: The missing Doxygen documentation for getPixel and analyzeJunctions has already been flagged in previous review comments.


78-78: LGTM!

bindings/py/src/img2num_pybind.cpp (1)

169-170: LGTM!

Also applies to: 181-181, 191-191

core/src/internal/image_to_svg.cpp (1)

20-20: LGTM!

packages/js/safeWasmWrappers.js (2)

187-187: LGTM!

Also applies to: 195-198


225-225: LGTM!

Also applies to: 234-237

core/src/internal/graph.cpp (3)

281-298: Shared-loop mode drops hole semantics from m_contours.

This repopulates Node::m_contours with every returned ring, but hard-codes hierarchy to {-1, -1, -1, -1} and is_hole to false for all of them. That breaks the existing contour contract for regions with interior voids or nested islands.


260-279: Add regression coverage before making the shared-edge path the default.

compute_contours() now always takes the new shared-loop path, but this PR does not include tests for the reported gap artifact or for topology-sensitive cases.


472-522: LGTM!

core/include/img2num.h (1)

75-76: LGTM!

core/src/internal/labels_to_svg.cpp (1)

189-190: LGTM!

Also applies to: 208-208

bindings/c/include/cimg2num.h (1)

79-80: LGTM!

bindings/c/src/cimg2num.cpp (1)

10-24: LGTM!

Also applies to: 26-40, 80-94

bindings/js/src/wasm_wrapper.c (1)

35-55: LGTM!

Comment thread bindings/c/include/cimg2num.h
Comment thread bindings/py/src/img2num_pybind.cpp Outdated
Comment on lines +116 to +126
int min_area, int min_thickness) {
const uint8_t *data_ptr{static_cast<const uint8_t *>(data.request().ptr)};
const int32_t *labels_ptr{static_cast<const int32_t *>(labels.request().ptr)};

std::string svg{img2num::labels_to_svg(data_ptr, labels_ptr, width, height, min_area)};
std::string svg{img2num::labels_to_svg(data_ptr, labels_ptr, width, height, min_area, min_thickness)};
pybind11::str svg_py_str(std::move(svg));

return svg_py_str;
},
pybind11::arg("data"), pybind11::arg("labels"), pybind11::arg("width"),
pybind11::arg("height"), pybind11::arg("min_area"), "Convert labels to SVG string");
pybind11::arg("height"), pybind11::arg("min_area"), pybind11::arg("min_thickness"), "Convert labels to SVG string");

@coderabbitai coderabbitai Bot Jun 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Add a default value for min_thickness to prevent breaking existing code.

The min_thickness parameter is added as a required positional argument without a default value. This is a breaking change that will cause existing Python code calling labels_to_svg to fail. Provide a sensible default (e.g., 0 to disable or 10 to match the JS binding):

🔧 Proposed fix
         pybind11::arg("data"), pybind11::arg("labels"), pybind11::arg("width"),
-        pybind11::arg("height"), pybind11::arg("min_area"), pybind11::arg("min_thickness"), "Convert labels to SVG string");
+        pybind11::arg("height"), pybind11::arg("min_area"), pybind11::arg("min_thickness") = 0, "Convert labels to SVG string");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@bindings/py/src/img2num_pybind.cpp` around lines 116 - 126, The pybind
wrapper for labels_to_svg currently requires min_thickness as a positional
argument, breaking existing callers; update the pybind binding for the lambda
that calls img2num::labels_to_svg so the pybind11::arg("min_thickness") has a
default value (e.g., = 10 to match the JS binding or = 0 to disable), leaving
the lambda signature and call unchanged but changing the argument declaration to
include that default and updating the final docstring if needed.

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Krasner, I thought we already had this done. I'm sorry for missing - this is very important.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it fail? Internally, the C++ applies a default value. Look at core/include/img2num.h - it applies the zero there.

Are you saying that Python will throw an error?

Comment thread core/include/img2num.h
Comment thread core/src/internal/graph.cpp Outdated
int j = 0;
for (const Node_ptr &n : get_nodes()) {
if (n->area() == 0) continue;
float eps = 0.25f;

@coderabbitai coderabbitai Bot Jun 8, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | 💤 Low value

Use brace initialization for consistency.

Per repository conventions, prefer brace initialization over copy initialization for local variables.

♻️ Suggested fix
-        float eps = 0.25f;
+        float eps{0.25f};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/src/internal/graph.cpp` at line 270, The local variable eps uses copy
initialization; update its declaration in graph.cpp (variable eps) to use brace
initialization per repo convention by declaring eps with braces around its
initializer (retain the 0.25f value) instead of using the equals-style
initialization.

Source: Learnings

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Krasner is this meant to represent $\epsilon$ (epsilon)?

Additionally, why is this passed through multiple functions all the way down to fit_curve_reduction (as tolerance), then over to fitRecursive (as errorLimit)?

flowchart TD
  A["Graph::compute_contours<br /><sub>float eps = 0.25f;</sub>"] --> B[build_shared_loops]
  B --> C["fit_edge"]
  C --> D["fit_curve_reduction"]
  D --> E["fitRecursive"]
Loading

It's a hard-coded value that just gets delegated down the call stack. In React, this would be called prop-drilling and it's generally a sign of vulnerable code - I'm not saying that there's anything wrong with it, I'm just making a suggestion and want to see if you had a particular motivation for this.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

epsilon is meant to control the error of the bezier fit. it does get pushed thru the build_shared_loops funciton to fit_edge to fit_curve_reduction. do you propose a better method?

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure. If it is meant to work that way, it's fine. That entire Graph function is a bit confusing, though because there's a fair bit of dead code:

It contains an if (true) as well as an else block - the compiler will just strip the if-check and the else block entirely.

Comment thread core/src/internal/graph.cpp Outdated
img2num_kmeans(img_data, out_data, out_labels, width, height, 16, 100, 1);
// Generate SVG
char* res_svg = img2num_labels_to_svg(img_data, out_labels, width, height, 100);
char* res_svg = img2num_labels_to_svg(img_data, out_labels, width, height, 100, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Add an inline comment explaining the min_thickness parameter.

As per coding guidelines, example applications must be a good reflection of how to use Img2Num's library with good comments for external users. The hardcoded 0 for min_thickness lacks explanation. Add a brief comment to help users understand this parameter:

📝 Suggested comment
-    char* res_svg = img2num_labels_to_svg(img_data, out_labels, width, height, 100, 0);
+    // min_area=100, min_thickness=0 (disabled)
+    char* res_svg = img2num_labels_to_svg(img_data, out_labels, width, height, 100, 0);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
char* res_svg = img2num_labels_to_svg(img_data, out_labels, width, height, 100, 0);
// min_area=100, min_thickness=0 (disabled)
char* res_svg = img2num_labels_to_svg(img_data, out_labels, width, height, 100, 0);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@example-apps/console-c/main.c` at line 74, Add an inline comment next to the
call to img2num_labels_to_svg explaining the sixth parameter (min_thickness):
state that min_thickness is the minimum stroke/segment thickness in pixels to
include in the SVG (use 0 to disable thickness filtering), suggest typical
values (e.g. 1–3 for thin fonts) and note its effect (filters out tiny noisy
components). Update the comment near the img2num_labels_to_svg(...) invocation
to briefly describe this behavior and recommended use.

Source: Coding guidelines

Comment thread example-apps/console-cpp/main.cpp Outdated
Comment thread example-apps/console-cpp/main.cpp Outdated
Comment thread example-apps/console-py/main.py
Comment thread example-apps/console-py/main.py Outdated
@Krasner

Krasner commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

@Ryan-Millard not sure what the wheel building failure in the python ci build is caused by. locally I'm able to do uv build in the docker container and build the wheel

@Ryan-Millard
Ryan-Millard changed the base branch from main to dev June 9, 2026 16:19
@Ryan-Millard

Copy link
Copy Markdown
Owner

@Krasner, I'm not sure. I think it was because you set the PR to target main instead of dev, which probably made it run using the workflows on main instead. The workflows on main were broken at a point in time and I'm not sure if dev even has the fixed versions.

@Krasner

Krasner commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

@Krasner, I'm not sure. I think it was because you set the PR to target main instead of dev, which probably made it run using the workflows on main instead. The workflows on main were broken at a point in time and I'm not sure if dev even has the fixed versions.

correct. dev has the updated Dockerfile and associated image that don't match main. Let me resolve the conflicts and then try against the dev branch...

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Krasner, I'm not sure. I think it was because you set the PR to target main instead of dev, which probably made it run using the workflows on main instead. The workflows on main were broken at a point in time and I'm not sure if dev even has the fixed versions.

correct. dev has the updated Dockerfile and associated image that don't match main. Let me resolve the conflicts and then try against the dev branch...

It'll still fail because I broke the image the other day.😂😂

I'm still trying to fix the permissions errors in #423 - it just takes so long to build the stuff.

@Krasner

Krasner commented Jun 9, 2026

Copy link
Copy Markdown
Collaborator Author

/docker-build b4ee348

Comment thread Dockerfile.dev Outdated
@github-actions

github-actions Bot commented Jun 9, 2026

Copy link
Copy Markdown
Contributor

🐳 Docker image built successfully!

Image

ghcr.io/ryan-millard/img2num-dev:pr-429

Run it locally:

  • Bash:
    ./img2num --ghcr-img pr-429 sh
  • Powershell:
    .\img2num.ps1 --ghcr-img pr-429 sh
  • CMD:
    .\img2num.bat --ghcr-img pr-429 sh

@Krasner

Krasner commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

@Ryan-Millard feel free to merge when you're ready

I think I need to have a better look at this. I seem to have missed a few things (probably because it's late and I'm a bit distracted).

no worries. it's hard to find comments in the diff viewer, but to address one comment in the Graph::compute_contours method, I do have a big if (true) block. The else block is the old method. I can delete it but kept it there just in case...

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Ryan-Millard feel free to merge when you're ready

I think I need to have a better look at this. I seem to have missed a few things (probably because it's late and I'm a bit distracted).

no worries. it's hard to find comments in the diff viewer, but to address one comment in the Graph::compute_contours method, I do have a big if (true) block. The else block is the old method. I can delete it but kept it there just in case...

In case of what? There are also cleaner ways to go about keeping old code you don't have a use for - maybe git stash it or create a branch to store it. There has to be another solution and I'm going to look for it now.

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Ryan-Millard feel free to merge when you're ready

I think I need to have a better look at this. I seem to have missed a few things (probably because it's late and I'm a bit distracted).

no worries. it's hard to find comments in the diff viewer, but to address one comment in the Graph::compute_contours method, I do have a big if (true) block. The else block is the old method. I can delete it but kept it there just in case...

In case of what? There are also cleaner ways to go about keeping old code you don't have a use for - maybe git stash it or create a branch to store it. There has to be another solution and I'm going to look for it now.

Since it's the old version, we can use git to get it back (it would be what's called "restoring the hunk" where the hunk is the block of code that changed). It's very simple to do - you just need to be able to use git blame, some diffs and restore/similar.

@Krasner

Krasner commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

@Ryan-Millard feel free to merge when you're ready

I think I need to have a better look at this. I seem to have missed a few things (probably because it's late and I'm a bit distracted).

no worries. it's hard to find comments in the diff viewer, but to address one comment in the Graph::compute_contours method, I do have a big if (true) block. The else block is the old method. I can delete it but kept it there just in case...

In case of what? There are also cleaner ways to go about keeping old code you don't have a use for - maybe git stash it or create a branch to store it. There has to be another solution and I'm going to look for it now.

I can think of a few cases:

  1. We find that the output is unsatisfactory - though it has looked decent for all the images that I've thrown at it
  2. A new developer may want to see previous method / chains of thought / approaches that have been tried in case they want to propose a better method (this is probably the more important case).

Sure the one could scrub through various branches, but this is a core part of the algorithm that have not yet been fully stabilized, so having clearer visibility to different versions might be useful

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Ryan-Millard feel free to merge when you're ready

I think I need to have a better look at this. I seem to have missed a few things (probably because it's late and I'm a bit distracted).

no worries. it's hard to find comments in the diff viewer, but to address one comment in the Graph::compute_contours method, I do have a big if (true) block. The else block is the old method. I can delete it but kept it there just in case...

In case of what? There are also cleaner ways to go about keeping old code you don't have a use for - maybe git stash it or create a branch to store it. There has to be another solution and I'm going to look for it now.

I can think of a few cases:

  1. We find that the output is unsatisfactory - though it has looked decent for all the images that I've thrown at it
  2. A new developer may want to see previous method / chains of thought / approaches that have been tried in case they want to propose a better method (this is probably the more important case).

Sure the one could scrub through various branches, but this is a core part of the algorithm that have not yet been fully stabilized, so having clearer visibility to different versions might be useful

I think suggesting that they run git diffing on the function would be the best solution. Since it's been around for a while and hasn't changed, they wouldn't need to be very accurate between the commits.

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Krasner please will you also update the README.md and maybe the Docusaurus homepage if there are any necessary updates. I think that the README.md update may be a good idea.

@Krasner

Krasner commented Jun 15, 2026

Copy link
Copy Markdown
Collaborator Author

@Krasner please will you also update the README.md and maybe the Docusaurus homepage if there are any necessary updates. I think that the README.md update may be a good idea.

Is there anything there to update?

I'm going to have a big documentation PR ready soon like #388 (which can be closed for now) where I will update as much documentation as I can for all the latest changes and new stuff (python bindings)

@Ryan-Millard

Copy link
Copy Markdown
Owner

@Krasner please will you also update the README.md and maybe the Docusaurus homepage if there are any necessary updates. I think that the README.md update may be a good idea.

Is there anything there to update?

I'm going to have a big documentation PR ready soon like #388 (which can be closed for now) where I will update as much documentation as I can for all the latest changes and new stuff (python bindings)

I was talking about the images. Do you think it would be a good idea to update them?

@Krasner

Krasner commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator Author

I was talking about the images. Do you think it would be a good idea to update them?

Yes good point. I've updated them

@Krasner
Krasner requested a review from Ryan-Millard June 18, 2026 01:49
Ryan-Millard
Ryan-Millard previously approved these changes Jun 19, 2026

@Ryan-Millard Ryan-Millard left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Krasner this all looks good and it can be merged as-is. Please just address the comments I left as well as run this:

pnpm format:cpp # there's a bug with the format script

Comment thread bindings/c/src/cimg2num.cpp Outdated
Comment on lines 95 to 109
char *img2num_labels_to_svg(const uint8_t *data, const int32_t *labels, const int width,
const int height, const int min_area, const int min_thickness) {
char *result{nullptr};
img2num::clear_last_error_and_catch(
[&](const uint8_t* d, const int32_t* l, const int w, const int h, const int min_a) {
std::string svg {img2num::labels_to_svg(d, l, w, h, min_a)};
result = static_cast<char*>(std::malloc(svg.size() + 1));
[&](const uint8_t *d, const int32_t *l, const int w, const int h, const int min_a, const int min_t) {
std::string svg{img2num::labels_to_svg(d, l, w, h, min_a, min_t)};
result = static_cast<char *>(std::malloc(svg.size() + 1));
if (!result) {
return; // Allocation failed
}
std::memcpy(result, svg.c_str(), svg.size() + 1);
},
data, labels, width, height, min_area
);
data, labels, width, height, min_area, min_thickness);
return result;
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are your thoughts on us removing this function entirely in the future (not now) to make it easier to maintain the library: we only expose one interface for converting to SVGs.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd still keep it... it's not difficult to maintain

Comment thread bindings/py/src/img2num_pybind.cpp Outdated
Comment on lines +116 to +126
int min_area, int min_thickness) {
const uint8_t *data_ptr{static_cast<const uint8_t *>(data.request().ptr)};
const int32_t *labels_ptr{static_cast<const int32_t *>(labels.request().ptr)};

std::string svg{img2num::labels_to_svg(data_ptr, labels_ptr, width, height, min_area)};
std::string svg{img2num::labels_to_svg(data_ptr, labels_ptr, width, height, min_area, min_thickness)};
pybind11::str svg_py_str(std::move(svg));

return svg_py_str;
},
pybind11::arg("data"), pybind11::arg("labels"), pybind11::arg("width"),
pybind11::arg("height"), pybind11::arg("min_area"), "Convert labels to SVG string");
pybind11::arg("height"), pybind11::arg("min_area"), pybind11::arg("min_thickness"), "Convert labels to SVG string");

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would it fail? Internally, the C++ applies a default value. Look at core/include/img2num.h - it applies the zero there.

Are you saying that Python will throw an error?

Comment thread core/include/internal/douglas_peucker.h Outdated
Comment on lines +42 to +55
/**
* `@brief` Safely retrieves a pixel value from a binary image with bounds checking.
*
* `@param` img Binary image buffer
* `@param` w Image width
* `@param` h Image height
* `@param` x Pixel x-coordinate
* `@param` y Pixel y-coordinate
* `@return` Pixel value at (x, y), or 0 if out of bounds
*/
inline uint8_t getPixel(const std::vector<uint8_t>& img, int w, int h, int x, int y) {
if (x < 0 || x >= w || y < 0 || y >= h) return 0; // Boundary check
return img[y * w + x];
}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should probably schedule a refactor to make the library use the image class wherever possible because we repeat this pattern a lot.

Comment thread core/src/internal/douglas_peucker.cpp Outdated
Comment thread core/src/internal/graph.cpp Outdated
Comment thread core/src/internal/shared_contours.cpp Outdated
Comment thread core/src/internal/shared_contours.cpp Outdated
Comment thread core/src/internal/shared_contours.cpp Outdated
Comment thread core/src/internal/shared_contours.cpp Outdated
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

build-system c/c++ Changes to C or C++ files core documentation example-app Updates to code related to demonstration (example) applications js/ts

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants