Skip to content

Conversation

@Shane32
Copy link
Owner

@Shane32 Shane32 commented Oct 8, 2025

Changes:

  • Uses paths to create a SVG versus many individual rectangles
  • Supports transparent or partially-transparent colors - for both light and dark modules
  • Added GetGraphic() overload with no arguments for a scalable SVG (no width/height set) with black on white
  • Draws the light color as a background (assuming the dark color has no transparency)
  • Omits drawing the light or dark color if they are completely transparent
  • Draws the light color behind logos, as was done with the previous algorithm
  • 70% file size reduction or so
  • Numbers are all encoded with invariant culture
  • Considerable memory reduction
    • Only main StringBuilder allocates memory (assuming no logo in use), and final stringBuilder.ToString()
    • No matrix for 2d compression of rectangles
    • Encoding of numbers is done with a stackalloc and copied directly into the StringBuilder
  • RLE encoding of rectangles per row
  • Elimination of SVG v1.1 marker, an optional marker; generated SVG should be compatible with both 1.1 and 2.0 (unless the user specifies an RGBA color value besides the keyword transparent)

Todo:

  • Verify that the light color should be drawn behind logos
  • Add transparency tests

Notes:

  1. viewBox argument no longer affects the actual SVG view box. According to online documentation, the view box has no bearing whatsoever on the rendered size of the SVG. The view box just tells the viewer (e.g. browser) what portion of an infinitely large 2-dimensional coordinate space within the SVG to display. Only the width and height SVG tag parameters affect the displayed size. The only way this would affect a user/developer is if they were altering the SVG content after its generation. It should not affect embedding the SVG inside another SVG, as the embedded SVG coordinate system is its own.

  2. Also, the RLE encoding in a single dimension replaced a 2-dimensional rectangle detection algorithm previously in use. I find the previous algorithm to have very little usefulness beyond the finder boxes. In the future, it would be best to have an algorithm that would encode adjoining modules of any shape, which is certainly possible using even-odd fill. (Applies to PDF writing as well.) But it would come at the cost of additional memory requirements.

  3. The rendered SVG within the transposition tests still passes, since the SVG when rendered to a bitmap has not changed with this PR. Visual inspection of all prior tests indicate that the SVG produces identical output visually.

Summary by CodeRabbit

  • New Features

    • Added a parameterless SVG export, explicit sizing modes, hex color inputs, and improved logo placement and obstruction handling.
  • Refactor

    • Streamlined SVG assembly and numeric formatting for more consistent, size-aware SVG output and transparency handling.
  • Chores

    • Updated public documentation to reflect sizing, color, and logo behavior changes.
  • Tests

    • Added tests for unscaled output, color/hex/ARGB inputs, transparency, and logo scenarios.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 8, 2025

📝 Walkthrough

Walkthrough

Adds a parameterless SvgQRCode.GetGraphic(), consolidates SVG rendering into a unified hex-based path, introduces explicit SizingMode values, adds color/alpha and logo-placement helpers, switches module rendering to path-based run-length encoding, adjusts SVG attribute emission, and updates tests and API snapshots.

Changes

Cohort / File(s) Summary
SVG core & public API
QRCoder/SvgQRCode.cs
Adds public string GetGraphic(); consolidates existing overloads into a single internal rendering path; adds hex color routing and explicit SizingMode enum values; updates XML docs and public signatures.
Rendering, colors & logo handling
QRCoder/SvgQRCode.cs
Adds ColorToHex, GetTransparency, IsPartiallyTransparent, IsFullyTransparent, GetLogoAttributes, IsBlockedByLogo; conditional xmlns:xlink emission; emits width/height or viewBox per SizingMode; implements path/run-length module rendering via DrawModulesPath; integrates logo rendering and obstruction logic.
Helpers & formatting
QRCoder/Extensions/StringExtensions.cs, QRCoder/SvgQRCode.cs
Adds AppendInvariant for ints/floats to StringBuilder; replaces double-based formatting with CleanSvgVal(float); uses invariant numeric appends when building SVG path data.
PDF path numeric formatting
QRCoder/PdfByteQRCode.cs
Replaces ToStr(...) calls with AppendInvariant(...) in CreatePathFromModules; updates float formatting to G7.
Tests
QRCoderTests/SvgQRCodeRendererTests.cs
Adds unscaled SVG render test plus parameterized tests covering ARGB/hex color inputs, transparency cases, and logo interactions across ECC levels.
API approval snapshots
QRCoderApiTests/.../QRCoder.approved.txt
Updates API snapshots across target frameworks to include the new parameterless GetGraphic() and adjusted public signatures.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Caller
  participant SvgQRCode
  participant Matrix as ModuleMatrix
  participant PathBuilder
  participant LogoHelper

  Caller->>SvgQRCode: GetGraphic(...) or GetGraphic()
  SvgQRCode->>SvgQRCode: Normalize inputs (viewBox, colors -> hex, sizingMode)
  alt WidthHeightAttribute
    SvgQRCode->>SvgQRCode: Emit width & height attributes
  else ViewBoxAttribute
    SvgQRCode->>SvgQRCode: Emit viewBox attribute only
  end
  opt non-embedded logo
    SvgQRCode->>SvgQRCode: Add xmlns:xlink
  end
  opt background (non-transparent light color)
    SvgQRCode->>SvgQRCode: Emit background <rect>
  end
  SvgQRCode->>Matrix: Read QR module matrix
  SvgQRCode->>PathBuilder: Build run-length encoded dark-module path (skip modules blocked by logo)
  PathBuilder-->>SvgQRCode: Return path data
  opt logo provided
    SvgQRCode->>LogoHelper: Compute logo attributes & blocked modules
    LogoHelper-->>SvgQRCode: logo bounds & attributes
    SvgQRCode->>SvgQRCode: Render embedded `<svg>` or `<image>` with proper layering
  end
  SvgQRCode-->>Caller: Return assembled SVG string
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • gfoidl
  • codebude

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title clearly and concisely describes the primary enhancement of the pull request, namely the introduction of path-based SVG rendering combined with run-length encoding to improve file size and memory usage. It directly reflects the main objectives without unnecessary detail or extraneous information. The phrasing is specific and understandable to reviewers scanning the change history.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch svg

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 58789f5 and 790d32f.

📒 Files selected for processing (1)
  • QRCoder/PdfByteQRCode.cs (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/PdfByteQRCode.cs (1)
QRCoder/Extensions/StringExtensions.cs (3)
  • AppendInvariant (64-79)
  • AppendInvariant (86-101)
  • ToString (55-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: additional-tests
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: Test .NET Framework 4.6.2
🔇 Additional comments (2)
QRCoder/PdfByteQRCode.cs (2)

207-212: LGTM! Efficient integer formatting optimization.

Replacing ToStr() with AppendInvariant() eliminates intermediate string allocations by formatting integers directly into the StringBuilder. On platforms with span support, this also uses stackalloc for improved memory efficiency while maintaining invariant culture formatting.


245-249: LGTM! Documentation remains accurate.

The XML documentation correctly describes the method's behavior. The implementation continues to use the "G7" format with invariant culture for consistent PDF float formatting.


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.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 696bb39 and e8b7054.

⛔ Files ignored due to path filters (13)
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_from_helper.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_simple.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_viewbox_mode.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_viewbox_mode_viewboxattr.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_with_png_logo_bitmap.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_with_png_logo_bitmap_without_background.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_with_png_logo_bitmap_without_quietzones.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_with_png_logo_bytearray.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_with_svg_logo_embedded.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_with_svg_logo_image_tag.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_without_quietzones.approved.svg is excluded by !**/*.svg
  • QRCoderTests/SvgQRCodeRendererTests.can_render_svg_qrcode_without_quietzones_hex.approved.svg is excluded by !**/*.svg
📒 Files selected for processing (1)
  • QRCoder/SvgQRCode.cs (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/SvgQRCode.cs (1)
QRCoder/PostscriptQRCode.cs (1)
  • CleanSvgVal (139-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: additional-tests

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8b7054 and 5be3566.

📒 Files selected for processing (1)
  • QRCoder/SvgQRCode.cs (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/SvgQRCode.cs (1)
QRCoder/PostscriptQRCode.cs (1)
  • CleanSvgVal (139-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: additional-tests

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
QRCoder/SvgQRCode.cs (1)

154-169: Critical: Prevent NaN/Infinity logo coordinates when viewBox is zero.

This is a repeat of an unresolved issue from a previous review. When sizingMode == ViewBoxAttribute, the documentation states that the viewBox argument is unused, and callers can pass Size.Empty. However, lines 160-161 still divide by viewBox.Width/Height, producing Infinity when dimensions are zero. This causes:

  1. NaN/Infinity values in the generated <image> attributes
  2. Broken IsBlockedByLogo logic, preventing modules from being cleared behind the logo

Apply this diff to guard against zero dimensions:

+        var logoViewBox = viewBox;
+        if (logoViewBox.Width <= 0 || logoViewBox.Height <= 0)
+        {
+            logoViewBox = new Size(drawableModulesCount, drawableModulesCount);
+        }
-        var viewBoxLogoAttr = GetLogoAttributes(logo, viewBox);
+        var viewBoxLogoAttr = GetLogoAttributes(logo, logoViewBox);
         // Convert from viewBox coordinates to module coordinates
-        double scaleX = drawableModulesCount / (double)viewBox.Width;
-        double scaleY = drawableModulesCount / (double)viewBox.Height;
+        double scaleX = drawableModulesCount / (double)logoViewBox.Width;
+        double scaleY = drawableModulesCount / (double)logoViewBox.Height;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5be3566 and 96159c8.

📒 Files selected for processing (1)
  • QRCoder/SvgQRCode.cs (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/SvgQRCode.cs (1)
QRCoder/PostscriptQRCode.cs (1)
  • CleanSvgVal (139-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: additional-tests
🔇 Additional comments (9)
QRCoder/SvgQRCode.cs (9)

29-106: LGTM!

The documentation updates correctly describe the new sizingMode and logo parameters, and clarify when pixelsPerModule and viewBox are applicable.


112-137: LGTM!

The SVG header construction is well-structured, conditionally including namespace declarations and sizing attributes based on the logo presence and sizing mode.


139-152: LGTM!

The transparency-aware rendering strategy is well-designed, optimizing for the common case where dark color is fully opaque by drawing light color as a single background rectangle.


171-181: LGTM!

The conditional path drawing logic correctly handles transparency cases, skipping unnecessary draws when colors are fully transparent.


218-280: LGTM!

The RLE path encoding implementation is well-designed, combining consecutive horizontal modules into single path commands for optimal file size. The logo blocking logic correctly ensures light-colored background behind logos.


296-306: LGTM!

The IsBlockedByLogo implementation correctly converts module coordinates to viewBox space and checks for rectangle overlap.


308-329: LGTM!

The GetLogoAttributes helper and ImageAttributes struct correctly calculate centered logo positioning. Note that the zero viewBox issue is already flagged in a separate comment.


332-335: LGTM! Consistent with PostscriptQRCode.

The format change from G15 to G7 maintains consistency with PostscriptQRCode.cs and follows the .NET Framework vs Core compatibility guidance.

Based on relevant code snippets.


337-399: LGTM!

The transparency helper methods correctly handle:

  • The literal "transparent" keyword
  • Hex colors with alpha channels (#RRGGBBAA and #RGBA)
  • 4-bit to 8-bit alpha conversion (multiply by 17)
  • Default to fully opaque (255) for colors without alpha

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (2)
QRCoder/SvgQRCode.cs (2)

158-168: Prevent NaN/Infinity logo coordinates when viewBox is zero.

This issue was previously flagged: When sizingMode == ViewBoxAttribute, callers pass Size.Empty for the viewBox, but the code still divides by viewBox.Width/Height, producing Infinity/NaN that corrupts the <image> attributes and breaks IsBlockedByLogo logic.

Apply the previously suggested fix to guard against zero dimensions:

-            var viewBoxLogoAttr = GetLogoAttributes(logo, viewBox);
-            // Convert from viewBox coordinates to module coordinates
-            double scaleX = drawableModulesCount / (double)viewBox.Width;
-            double scaleY = drawableModulesCount / (double)viewBox.Height;
+            var logoViewBox = viewBox;
+            if (logoViewBox.Width <= 0 || logoViewBox.Height <= 0)
+            {
+                logoViewBox = new Size(drawableModulesCount, drawableModulesCount);
+            }
+            var viewBoxLogoAttr = GetLogoAttributes(logo, logoViewBox);
+            // Convert from viewBox coordinates to module coordinates
+            double scaleX = drawableModulesCount / (double)logoViewBox.Width;
+            double scaleY = drawableModulesCount / (double)logoViewBox.Height;

283-294: Critical: Fix double-append bug in Append helper.

This issue was previously flagged: When HAS_SPAN is defined, the number is appended twice—once from the buffer (line 289) and again from ToString (line 293), corrupting the SVG output by duplicating all numeric values.

Apply the previously suggested fix:

 void Append(int num)
 {
 #if HAS_SPAN
     Span<char> buffer = stackalloc char[16];
     if (num.TryFormat(buffer, out int charsWritten, default, CultureInfo.InvariantCulture))
     {
         svgFile.Append(buffer.Slice(0, charsWritten));
+        return;
     }
 #endif
     svgFile.Append(num.ToString(CultureInfo.InvariantCulture));
 }
🧹 Nitpick comments (1)
QRCoder/SvgQRCode.cs (1)

343-384: Consider validation for malformed color strings.

The GetTransparency method returns 255 (fully opaque) for malformed hex colors. While this is defensive programming, it may silently hide user errors in color specification (e.g., "#foo" or "#12345").

Consider adding explicit validation or logging for invalid color formats to help developers catch mistakes during testing, especially since this is a new transparency feature.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 96159c8 and c640bec.

📒 Files selected for processing (1)
  • QRCoder/SvgQRCode.cs (10 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/SvgQRCode.cs (1)
QRCoder/PostscriptQRCode.cs (1)
  • CleanSvgVal (139-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: additional-tests
  • GitHub Check: Test .NET Core 2.1
🔇 Additional comments (2)
QRCoder/SvgQRCode.cs (2)

410-415: LGTM: Explicit enum values.

Adding explicit values to the SizingMode enum is good practice and maintains backward compatibility since they match the implicit default values.


218-280: Verify path rendering logic with transparency tests.

The DrawModulesPath local function implements RLE-based path generation with transparency-aware module selection. The logic for light modules (!isDarkModule || isBlockedByLogo) intentionally draws light color behind the logo when fillLogoBackground is true, which aligns with the PR objective "Draws the light color behind logos."

However, per the PR's TODO, transparency tests are still needed to confirm this behavior works correctly across different color combinations and logo configurations.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c640bec and 1e363d7.

📒 Files selected for processing (1)
  • QRCoder/SvgQRCode.cs (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/SvgQRCode.cs (1)
QRCoder/PostscriptQRCode.cs (7)
  • GetGraphic (31-35)
  • GetGraphic (46-50)
  • GetGraphic (61-65)
  • GetGraphic (74-75)
  • GetGraphic (86-87)
  • GetGraphic (98-132)
  • CleanSvgVal (139-139)
🪛 GitHub Check: additional-tests
QRCoder/SvgQRCode.cs

[failure] 271-271:
No overload for method 'IsBlockedByLogo' takes 4 arguments

🪛 GitHub Check: Test .NET 5.0 Windows
QRCoder/SvgQRCode.cs

[failure] 271-271:
No overload for method 'IsBlockedByLogo' takes 4 arguments

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: Test .NET Core 2.1

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
QRCoder/SvgQRCode.cs (2)

267-278: Verify logo background fill logic per PR objectives.

The ShouldDrawModule logic for light modules draws the light color behind the entire logo area, regardless of whether the underlying QR modules are dark or light (line 277: !isDarkModule || isBlockedByLogo). This means dark modules behind the logo are overridden with the light color before the logo is placed.

While this may be intentional to ensure a consistent background behind logos, it differs from typical QR code rendering where modules behind logos are simply cleared/removed. The PR objectives explicitly list "Verify that the light color should be drawn behind logos" as a TODO item.

Consider adding tests to verify this behavior works correctly when:

  1. The logo has transparency (verifying that the light background shows through)
  2. The logo is small and centered (ensuring dark modules around it aren't affected)
  3. Comparing output with/without FillLogoBackground() enabled

Do you want me to help generate test cases for these scenarios, or would you prefer to open an issue to track this verification task?


320-324: Consider parameter type consistency with PostscriptQRCode.

CleanSvgVal accepts float, while the similar method in PostscriptQRCode.cs (line ~138) accepts double. Although float is appropriate here since RectangleF properties are floats, consider either:

  1. Using double for consistency across QR code generators
  2. Documenting why float precision is sufficient for SVG output

Based on relevant code snippets

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e665c56 and 3f6dd5b.

📒 Files selected for processing (1)
  • QRCoder/SvgQRCode.cs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/SvgQRCode.cs (1)
QRCoder/PostscriptQRCode.cs (7)
  • GetGraphic (31-35)
  • GetGraphic (46-50)
  • GetGraphic (61-65)
  • GetGraphic (74-75)
  • GetGraphic (86-87)
  • GetGraphic (98-132)
  • CleanSvgVal (139-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: Test .NET Core 3.1
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: additional-tests
🔇 Additional comments (8)
QRCoder/SvgQRCode.cs (8)

26-33: LGTM: Parameterless convenience overload added.

The new parameterless GetGraphic() method provides a sensible default configuration. The use of Size.Empty with SizingMode.ViewBoxAttribute is appropriate since the viewBox parameter is unused in that mode (as documented), and the logo coordinate calculations at line 167 correctly use drawableModulesCount instead of the viewBox dimensions.


121-146: LGTM: Clean SVG tag construction.

The refactored SVG opening tag construction efficiently handles optional attributes:

  • Conditionally includes xmlns:xlink only when needed (non-embedded logos)
  • Properly omits width and height attributes for ViewBoxAttribute mode
  • Uses inline integer formatting via the local Append helper

This reduces SVG file size and improves maintainability.


148-161: LGTM: Efficient background rendering optimization.

The logic correctly determines when to draw the light color as a simple background rectangle versus as a path:

  • When dark color is fully opaque, light can be a single <rect> (lines 152-161)
  • When dark has transparency, light modules must be drawn as a path (line 173)

This optimization significantly reduces SVG file size for the common opaque-colors case.


282-293: LGTM: Efficient integer formatting with proper fallback.

The Append helper efficiently formats integers using TryFormat with stack allocation on modern frameworks, with a proper return statement (line 289) preventing the double-append bug that was flagged in previous reviews. The fallback to ToString ensures compatibility with older frameworks.


326-372: Verify error handling strategy for invalid color formats.

GetTransparency defaults to 255 (fully opaque) for any color string that doesn't match expected formats (line 371). This means invalid hex colors like "#GG0000" or typos in the hex string will silently be treated as opaque, potentially masking user errors.

Consider whether to:

  1. Keep the current lenient behavior (fail-safe to opaque)
  2. Throw an exception for malformed hex colors
  3. Add validation in public GetGraphic methods that accept string colors

The current behavior is fail-safe but might hide bugs. Document the intended error handling strategy.


390-422: LGTM: Clean color-to-hex conversion with optimizations.

The ColorToHex method includes thoughtful optimizations:

  • Shorthand notation for common colors (#000 and #FFF)
  • Omits alpha channel for fully opaque colors
  • Uses the transparent keyword for fully transparent colors
  • Includes alpha channel (#RRGGBBAA) only when needed

This contributes to the ~70% file size reduction mentioned in the PR objectives.


163-168: LGTM: Logo coordinate calculations fixed.

The logo attribute calculation now correctly uses new Size(drawableModulesCount, drawableModulesCount) instead of the potentially zero-valued viewBox parameter. This addresses the NaN/Infinity issue flagged in previous reviews when SizingMode.ViewBoxAttribute was used with Size.Empty.


216-264: LGTM: Efficient RLE path generation.

The DrawModulesPath local function implements run-length encoding (RLE) to combine horizontal runs of adjacent modules into single path commands. This optimization:

  • Significantly reduces the number of path commands (one per run vs. one per module)
  • Uses compact relative path notation (h for horizontal, v for vertical)
  • Correctly handles module boundaries and run detection

This is a key contributor to the ~70% file size reduction mentioned in the PR objectives.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
QRCoder/SvgQRCode.cs (2)

320-324: Update comment to reflect float signature

The comment references "double values" but the method signature is now float. This is a minor documentation inconsistency.

Update the comment:

-    //Clean double values for international use/formats
+    //Clean float values for international use/formats
     //We use explicitly "G7" to avoid differences between .NET full and Core platforms
     //https://stackoverflow.com/questions/64898117/tostring-has-a-different-behavior-between-net-462-and-net-core-3-1
     private static string CleanSvgVal(float input)
         => input.ToString("G7", CultureInfo.InvariantCulture);

137-144: Validate viewBox dimensions for WidthHeightAttribute

Throw an exception if viewBox.Width or viewBox.Height ≤ 0 to avoid emitting width="0" height="0":

 if (sizingMode == SizingMode.WidthHeightAttribute)
 {
+    if (viewBox.Width <= 0 || viewBox.Height <= 0)
+        throw new ArgumentException("ViewBox must have positive width and height when using WidthHeightAttribute sizing mode.", nameof(viewBox));
     svgFile.Append(@" width=""");
     Append(viewBox.Width);
     // …
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f6dd5b and b2c8d19.

📒 Files selected for processing (1)
  • QRCoder/SvgQRCode.cs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/SvgQRCode.cs (1)
QRCoder/PostscriptQRCode.cs (7)
  • GetGraphic (31-35)
  • GetGraphic (46-50)
  • GetGraphic (61-65)
  • GetGraphic (74-75)
  • GetGraphic (86-87)
  • GetGraphic (98-132)
  • CleanSvgVal (139-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: additional-tests
🔇 Additional comments (7)
QRCoder/SvgQRCode.cs (7)

26-33: LGTM: Convenient parameterless overload

The new parameterless GetGraphic() method provides a simple default path for generating scalable SVG QR codes. The defaults (ViewBoxAttribute mode with black/white colors) are sensible.


104-104: LGTM: Clean consolidation through ColorToHex

Routing the Color-based overload through ColorToHex unifies the rendering path and ensures consistent hex-based color handling.


273-277: Verify the light color behind logos behavior

The current logic fills the logo area with light color when fillLogoBackground is true:

  • Dark modules: drawn only where not blocked by logo
  • Light modules: drawn everywhere except where blocked by dark modules, PLUS the entire logo area

This means the logo sits on a light background. The PR objectives include "Todo: Verify that the light color should be drawn behind logos."

Please confirm this is the intended behavior. Alternative approaches:

  1. Current: Light background behind logo (current implementation)
  2. Transparent: No fill behind logo (would require (!isDarkModule && !isBlockedByLogo) instead)
  3. Conditional: Different behavior based on logo transparency

If the current behavior is correct, please update the PR objectives to remove the TODO.


282-293: LGTM: Efficient integer appending with proper flow control

The Append helper correctly handles both Span-based and fallback paths with the return statement preventing the double-append issue flagged in previous reviews.


326-388: LGTM: Comprehensive transparency handling

The transparency helpers correctly parse alpha channels from multiple hex formats:

  • #RRGGBBAA: 8-bit alpha (lines 343-354)
  • #RGBA: 4-bit alpha converted to 8-bit via multiplication by 17 (lines 356-367)
  • "transparent" keyword: returns 0 (lines 337-338)
  • Default: returns 255 for opaque colors without alpha (line 371)

The math for 4-bit to 8-bit conversion (alpha * 17) is correct, mapping 0-15 to 0-255.


390-422: LGTM: Efficient color-to-hex conversion with shorthands

The ColorToHex helper optimizes common cases with shorthand formats (#000 for black, #FFF for white) while correctly handling transparency:

  • Fully opaque: #RRGGBB format
  • Fully transparent: "transparent" keyword
  • Partially transparent: #RRGGBBAA format

This contributes to the file size reduction mentioned in the PR objectives.


296-318: LGTM: Correct logo placement and collision detection

The helper methods implement correct logic:

  • IsBlockedByLogo: Proper rectangle overlap detection using edge comparisons
  • GetLogoAttributes: Correctly centers logo within viewBox using float arithmetic

@Shane32 Shane32 changed the title [WIP] Optimize SVGs Add path-based SVG rendering with RLE encoding to reduce file size and memory usage Oct 8, 2025
@Shane32 Shane32 self-assigned this Oct 8, 2025
@Shane32 Shane32 added this to the 1.7.0 milestone Oct 8, 2025
@Shane32 Shane32 added enhancement A new feature or feature request performance Performance related enhancements or benchmarks labels Oct 8, 2025
@Shane32 Shane32 requested a review from gfoidl October 8, 2025 04:29
@Shane32
Copy link
Owner Author

Shane32 commented Oct 8, 2025

@gfoidl Some PRs are small ... some PRs are big .... (lol)

Copy link
Collaborator

@gfoidl gfoidl left a comment

Choose a reason for hiding this comment

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

Just two minor things, otherwise LGTM.

Very impressive 😄.

Comment on lines 284 to 291
#if HAS_SPAN
Span<char> buffer = stackalloc char[16];
if (num.TryFormat(buffer, out int charsWritten, default, CultureInfo.InvariantCulture))
{
svgFile.Append(buffer.Slice(0, charsWritten));
return;
}
#endif
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
#if HAS_SPAN
Span<char> buffer = stackalloc char[16];
if (num.TryFormat(buffer, out int charsWritten, default, CultureInfo.InvariantCulture))
{
svgFile.Append(buffer.Slice(0, charsWritten));
return;
}
#endif
#if NET6_0_OR_GREATER
svgFile.Append(CultureInfo.InvariantCulture, $"{num}");
#endif

Should also (w/o measuring) be a little bit faster, as it's formatted to the SB's buffer directly, and not via the span detour.

For < .NET 6 where span is available I wouldn't bother and don't have a separate #ifdef for this.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I pushed this code into StringExtensions, with a fast path for .NET 6+, a fallback for HAS_SPAN, and fallback for anything else. Your argument is completely valid; we really don't need to optimize for older targets....but why not - so long as it doesn't impede reading the code? I think what I'd like to do is for v2 create an Append(IFormatProvider, FormattableString) method for pre-.NET6 environments and combine a bunch of these multi-line Append commands into a interpolated string for better readability and more concise code.

Copy link
Owner Author

Choose a reason for hiding this comment

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

I'd also like to consider a dependency on System.Memory for .NET Standard 2.0 support in v2 so that Span support is available throughout the codebase. We can review a PR and see if it makes sense or not. It may not help much if the framework (like int.Parse) does not support Span.

Copy link
Collaborator

Choose a reason for hiding this comment

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

if the framework (like int.Parse) does not support Span.

I think that could be polyfilled with C#'s extension members. So a new compiler creaets code that runs on old targets.
Thus I also think that System.Memory makes the code base cleaner.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Well this savings are impressive 👍🏻

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (2)
QRCoder/SvgQRCode.cs (2)

128-128: Consider verbatim string for readability.

As noted in past review, this line has many quote characters. Using a verbatim string could improve readability:

-        svgFile.Append("\" shape-rendering=\"crispEdges\"");
+        svgFile.Append(@"\" shape-rendering=\""crispEdges\""");

Or with raw string literals (C# 11+):

-        svgFile.Append("\" shape-rendering=\"crispEdges\"");
+        svgFile.Append("""" shape-rendering="crispEdges"""");

Based on learnings (past review comment by gfoidl)


282-293: Consider StringBuilder.AppendFormat for .NET 6+ performance.

As suggested in past review, for .NET 6+, using the interpolated string overload could be slightly faster as it formats directly to the StringBuilder's buffer:

+#if NET6_0_OR_GREATER
+            svgFile.Append(CultureInfo.InvariantCulture, $"{num}");
+#elif HAS_SPAN
-#if HAS_SPAN
             Span<char> buffer = stackalloc char[16];
             if (num.TryFormat(buffer, out int charsWritten, default, CultureInfo.InvariantCulture))
             {
                 svgFile.Append(buffer.Slice(0, charsWritten));
                 return;
             }
 #endif
             svgFile.Append(num.ToString(CultureInfo.InvariantCulture));

However, the current implementation is also performant and reasonable.

Based on learnings (past review comment by gfoidl)

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8ad2c67 and b2b37b4.

📒 Files selected for processing (1)
  • QRCoder/SvgQRCode.cs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/SvgQRCode.cs (1)
QRCoder/PostscriptQRCode.cs (1)
  • CleanSvgVal (139-139)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: additional-tests
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: Test .NET Core 3.1
🔇 Additional comments (4)
QRCoder/SvgQRCode.cs (4)

26-33: LGTM! Clean parameterless overload.

The new parameterless GetGraphic() method provides a sensible default for generating scalable SVGs (black on white, no width/height attributes). The delegation to the main method is clean and appropriate.


116-294: Excellent path-based rendering implementation!

The main rendering logic is well-designed:

  1. Correct transparency handling: The code properly distinguishes between fully opaque, partially transparent, and fully transparent colors, drawing light modules as either a background rect (when dark is opaque) or as a path (when dark is transparent) to ensure proper layering.

  2. Efficient RLE encoding: The run-length encoding for combining adjacent modules reduces file size significantly.

  3. Proper logo obstruction: The ShouldDrawModule logic correctly clears space for logos while maintaining QR code readability.

  4. Safe numeric formatting: The Append helper with stackalloc is efficient and the return statement on line 289 correctly prevents double-appending.

All previously reported critical issues (NaN/Infinity coordinates, double-append bug, extra argument) have been successfully addressed.


296-422: LGTM! Well-implemented helper methods.

The helper methods are well-designed and correct:

  1. IsBlockedByLogo: Proper rectangle overlap detection for logo obstruction.
  2. GetLogoAttributes: Correctly centers the logo based on percentage size.
  3. CleanSvgVal: Uses "G7" format for cross-platform consistency (good catch on the .NET Core vs Full Framework difference).
  4. GetTransparency: Robustly handles multiple hex formats (#RRGGBBAA, #RGBA) and the "transparent" keyword with proper bounds checking.
  5. ColorToHex: Efficiently uses shorthand notation for common colors and properly handles transparency.

427-438: LGTM! Explicit enum values improve stability.

Adding explicit values to the SizingMode enum is good practice for API stability and serialization. The updated documentation clearly explains each mode's behavior.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
QRCoder/SvgQRCode.cs (1)

121-146: Consider verbatim string for better readability.

The escaped quotes in line 128 can make the string harder to read. As suggested in past review comments, consider using a verbatim string literal for clarity.

-        svgFile.Append("\" shape-rendering=\"crispEdges\"");
+        svgFile.Append(@"\" shape-rendering=\"crispEdges""");

Based on learnings.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b2b37b4 and e87b31c.

📒 Files selected for processing (3)
  • QRCoder/Extensions/StringExtensions.cs (1 hunks)
  • QRCoder/PdfByteQRCode.cs (2 hunks)
  • QRCoder/SvgQRCode.cs (7 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
QRCoder/PdfByteQRCode.cs (1)
QRCoder/Extensions/StringExtensions.cs (2)
  • AppendInvariant (64-79)
  • AppendInvariant (86-101)
QRCoder/SvgQRCode.cs (2)
QRCoder/PostscriptQRCode.cs (1)
  • CleanSvgVal (139-139)
QRCoder/Extensions/StringExtensions.cs (3)
  • AppendInvariant (64-79)
  • AppendInvariant (86-101)
  • ToString (55-56)
QRCoder/Extensions/StringExtensions.cs (6)
QRCoder/PayloadGenerator/BitcoinLikeCryptoCurrencyAddress.cs (1)
  • ToString (46-65)
QRCoder/PayloadGenerator/Girocode.cs (1)
  • ToString (75-95)
QRCoder/PayloadGenerator/MoneroTransaction.cs (1)
  • ToString (40-48)
QRCoder/PayloadGenerator/SlovenianUpnQr.cs (1)
  • ToString (142-161)
QRCoder/PayloadGenerator/BezahlCode.cs (1)
  • ToString (247-328)
QRCoder/PayloadGenerator/SwissQrCode.cs (3)
  • ToString (293-294)
  • ToString (480-490)
  • ToString (544-598)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (6)
  • GitHub Check: Test .NET 6.0 Windows
  • GitHub Check: Test .NET 5.0 Windows
  • GitHub Check: Test .NET Framework 4.6.2
  • GitHub Check: Test .NET Core 2.1
  • GitHub Check: additional-tests
  • GitHub Check: Test .NET Core 3.1
🔇 Additional comments (8)
QRCoder/PdfByteQRCode.cs (1)

207-212: LGTM!

The migration to AppendInvariant ensures consistent invariant culture formatting for numeric values in PDF path commands.

QRCoder/SvgQRCode.cs (7)

26-33: LGTM!

The parameterless overload provides a convenient default for generating scalable SVG without explicit sizing, routing through the unified rendering path with sensible defaults (black on white, ViewBoxAttribute mode).


148-180: LGTM!

The transparency-aware rendering logic correctly handles all cases:

  • Fully opaque dark with opaque/transparent light: light as background rect
  • Partially transparent dark: light modules drawn as path
  • Fully transparent colors: skipped entirely

This ensures proper layering and avoids unnecessary SVG elements.


216-280: LGTM!

The path-based rendering with RLE encoding is well-implemented:

  • Correctly combines adjacent modules into single rectangles
  • Uses efficient SVG path commands (M for move, h/v for relative lines, z for close)
  • ShouldDrawModule logic properly handles both dark/light modules and logo obstruction

This achieves the stated goal of ~70% file size reduction.


282-292: LGTM!

The rectangle overlap logic correctly detects when a module at (x,y) intersects with the logo area. The conditions properly check all four edges for overlap.


312-375: LGTM!

The transparency helpers correctly handle:

  • The "transparent" keyword → alpha 0
  • #RRGGBBAA format (9 chars) → extract alpha from last 2 hex digits
  • #RGBA format (5 chars) → extract alpha from last digit, multiply by 17 for 8-bit
  • Default → fully opaque (alpha 255)

The IsPartiallyTransparent and IsFullyTransparent predicates provide clear intent.


381-408: LGTM!

The ColorToHex conversion is well-optimized:

  • Uses shorthand #000 and #FFF for common black/white
  • Uses "transparent" keyword for fully transparent (better SVG compatibility than #RRGGBB00)
  • Uses #RRGGBB for opaque colors
  • Uses #RRGGBBAA only when necessary for partial transparency

This keeps the SVG output concise while maintaining correctness.


413-424: LGTM!

Explicitly assigning enum values (0 and 1) ensures stability for serialization and prevents potential breaking changes if new values are added.

Comment on lines 205 to 212
// Create a single rectangle for the entire run of dark modules
// Format: x y width height re
pathCommands.Append(ToStr(startX));
pathCommands.AppendInvariant(startX);
pathCommands.Append(' ');
pathCommands.Append(ToStr(y));
pathCommands.AppendInvariant(y);
pathCommands.Append(' ');
pathCommands.Append(ToStr(x - startX));
pathCommands.AppendInvariant(x - startX);
pathCommands.Append(" 1 re\r\n");
Copy link
Owner Author

Choose a reason for hiding this comment

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

Hopefully for v2 this can all be cleaned up into an interpolated string, fully optimized for .NET Standard 2.1+, but compatible with .NET Standard 2.0 as well.

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

Labels

enhancement A new feature or feature request performance Performance related enhancements or benchmarks

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants