Skip to content

Conversation

@Shane32
Copy link
Owner

@Shane32 Shane32 commented Oct 5, 2025

This changes the PdfByteQRCode algorithm to draw rectangles rather than to embed an image. This results in a PDF that has no compression artifacts and is about 50% the size of the previous JPG-embedded image (at default compression settings). It also allows the renderer to be compatible with any OS.

As an added benefit, the previous rendering process would convert to PNG first (using PngByteQRCode), then read it via GDI+, then save it via GDI+ to JPEG. All of that has been eliminated, making this code much faster. I did not perform any string optimizations within this PR as it was not within scope.

Extensive comments to the PDF commands were added so it is understood what the code actually does.

The jpgQuality parameter is now unused. While I could add a method without the parameter and mark the old method as obsolete, this would technically be a source-breaking change (not binary-breaking) in certain cases. So for v1.x I did not change it.

Summary by CodeRabbit

  • New Features

    • Generate QR codes directly as PDF bytes with vector-based rendering for crisp, scalable output.
    • Customizable dark/light colors and DPI; parameterless instantiation supported.
    • Cross-platform support (Windows-only restriction removed).
  • Changes

    • Simplified API: removed obsolete jpgQuality parameter; standardized GetGraphic signature and streamlined PDF output.
  • Tests

    • Added tests for black/white, color, custom DPI, parameterless usage, and helper-based PDF generation.

@Shane32 Shane32 added this to the 1.7.0 milestone Oct 5, 2025
@Shane32 Shane32 self-assigned this Oct 5, 2025
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Oct 5, 2025

📝 Walkthrough

Walkthrough

Implements in-memory PDF construction for PdfByteQRCode by drawing QR modules as PDF path commands, adds internal helpers for path/color/formatting, removes the jpgQuality parameter from a GetGraphic overload, removes Windows-only platform attributes, and adds unit tests for PDF QR rendering.

Changes

Cohort / File(s) Summary
PDF renderer
QRCoder/PdfByteQRCode.cs
Replaced image-based output with manual in-memory PDF construction (header, objects, content stream, xref, trailer). Renders QR as PDF path commands, computes module metrics and transformation matrix, fills background and dark-module paths, and adds helpers: CreatePathFromModules, ColorToPdfRgb, ToStr overloads. Conditional stream handling for NETFRAMEWORK vs NET6+.
API approval updates (platform & signature)
QRCoderApiTests/net60-windows/QRCoder.approved.txt
Removed [SupportedOSPlatform("windows")] attributes and updated GetGraphic signature to drop jpgQuality.
API approval baselines
QRCoderApiTests/net60/QRCoder.approved.txt, QRCoderApiTests/netstandard13/QRCoder.approved.txt
Baseline entries showing PdfByteQRCode and PdfByteQRCodeHelper public types and their GetGraphic/GetQRCode overloads (older snapshots include jpgQuality variants).
Unit tests
QRCoderTests/PdfByteQRCodeRendererTests.cs
Added tests validating PDF QR rendering: black/white, color, custom DPI, parameterless ctor, and helper-based generation using snapshot approvals.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant Helper as PdfByteQRCodeHelper
  participant Renderer as PdfByteQRCode
  participant PDF as In-memory PDF Builder

  Caller->>Helper: GetQRCode(plainText, pixelsPerModule, colors?, ecc...)
  Helper->>Renderer: new PdfByteQRCode(qrCodeData)
  Helper->>Renderer: GetGraphic(pixelsPerModule, darkHex, lightHex, dpi)
  Renderer->>PDF: Compute layout, transform, background rect
  Renderer->>PDF: CreatePathFromModules -> emit path commands
  Renderer->>PDF: Set colors (ColorToPdfRgb) and fill (even-odd)
  PDF-->>Renderer: byte[] PDF bytes
  Renderer-->>Helper: byte[] PDF bytes
  Helper-->>Caller: byte[] PDF bytes
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested reviewers

  • codebude
  • gfoidl

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 pull request title succinctly and accurately captures the primary change of modifying PdfByteQRCode to draw rectangles instead of embedding JPEGs, reflecting the main performance and cross-platform improvement described. It is specific to the PdfByteQRCode component and avoids vague language or noise. A teammate scanning the history can understand the core update from this title alone. No additional context is needed in the title.
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 pdf

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.

@Shane32 Shane32 added enhancement A new feature or feature request performance Performance related enhancements or benchmarks labels Oct 5, 2025
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

Caution

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

⚠️ Outside diff range comments (2)
QRCoder/PdfByteQRCode.cs (2)

62-131: Handle #RGB hex inputs without throwing.

HexColorToByteArray collapses shorthand HTML colors such as #FFF down to a single byte, so ColorToPdfRgb immediately throws because it insists on exactly three components. The prior System.Drawing-based pipeline accepted CSS shorthand via ColorTranslator.FromHtml, so this regresses existing callers. Please normalize 3-digit inputs (and reject other unsupported lengths) before converting to bytes.

         if (colorString.StartsWith("#"))
             colorString = colorString.Substring(1);
-        byte[] byteColor = new byte[colorString.Length / 2];
+
+        if (colorString.Length == 3)
+        {
+            colorString = new string(new[]
+            {
+                colorString[0], colorString[0],
+                colorString[1], colorString[1],
+                colorString[2], colorString[2]
+            });
+        }
+        else if (colorString.Length != 6)
+        {
+            throw new ArgumentException("HTML hex colors must contain exactly 3 or 6 digits.", nameof(colorString));
+        }
+
+        byte[] byteColor = new byte[colorString.Length / 2];
         for (int i = 0; i < byteColor.Length; i++)
             byteColor[i] = byte.Parse(colorString.Substring(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
         return byteColor;

15-18: Drop the Windows-only annotation.

The renderer no longer touches System.Drawing, yet the new build still tags PdfByteQRCode (and its helper) with [SupportedOSPlatform("windows")]. That keeps CA1416 warnings alive—and many callers treat those warnings as errors—so the advertised cross-platform gain never lands. Please remove the attribute blocks so the API is available without Windows gating and update the snapshots accordingly.

-#if NET6_0_OR_GREATER
-[System.Runtime.Versioning.SupportedOSPlatform("windows")]
-#endif
 // ReSharper disable once InconsistentNaming
 public class PdfByteQRCode : AbstractQRCode, IDisposable
@@
-#if NET6_0_OR_GREATER
-[System.Runtime.Versioning.SupportedOSPlatform("windows")]
-#endif
 /// <summary>
 /// Provides static methods for creating PDF byte array QR codes.

Also applies to: 305-307

🧹 Nitpick comments (1)
QRCoderTests/PdfByteQRCodeRendererTests.cs (1)

14-17: Dispose QR resources in the tests.

QRCodeGenerator, QRCodeData, and PdfByteQRCode are disposable; leaving them undisposed in tight test loops leaks handles and can taint later assertions. Please wrap each in using var (or helper) in the test bodies.

-        var gen = new QRCodeGenerator();
-        var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.L);
-        var pdfCodeGfx = new PdfByteQRCode(data).GetGraphic(5);
+        using var gen = new QRCodeGenerator();
+        using var data = gen.CreateQrCode("This is a quick test! 123#?", QRCodeGenerator.ECCLevel.L);
+        using var renderer = new PdfByteQRCode(data);
+        var pdfCodeGfx = renderer.GetGraphic(5);

Also applies to: 26-27

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 409db4c and 6f27ee9.

⛔ Files ignored due to path filters (5)
  • QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_blackwhite.approved.pdf is excluded by !**/*.pdf
  • QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_color.approved.pdf is excluded by !**/*.pdf
  • QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_custom_dpi.approved.pdf is excluded by !**/*.pdf
  • QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper.approved.pdf is excluded by !**/*.pdf
  • QRCoderTests/PdfByteQRCodeRendererTests.can_render_pdfbyte_qrcode_from_helper_2.approved.pdf is excluded by !**/*.pdf
📒 Files selected for processing (6)
  • QRCoder/PdfByteQRCode.cs (2 hunks)
  • QRCoderApiTests/net35+net40+net50+net50-windows+netstandard20+netstandard21/QRCoder.approved.txt (1 hunks)
  • QRCoderApiTests/net60-windows/QRCoder.approved.txt (1 hunks)
  • QRCoderApiTests/net60/QRCoder.approved.txt (1 hunks)
  • QRCoderApiTests/netstandard13/QRCoder.approved.txt (1 hunks)
  • QRCoderTests/PdfByteQRCodeRendererTests.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
QRCoder/PdfByteQRCode.cs (2)
QRCoder/PngByteQRCode.cs (2)
  • GetGraphic (37-49)
  • GetGraphic (60-61)
QRCoder/BitmapByteQRCode.cs (1)
  • HexColorToByteArray (143-151)
QRCoderTests/PdfByteQRCodeRendererTests.cs (1)
QRCoder/PdfByteQRCode.cs (8)
  • PdfByteQRCode (18-302)
  • PdfByteQRCode (41-41)
  • PdfByteQRCode (47-47)
  • GetGraphic (54-55)
  • GetGraphic (141-274)
  • PdfByteQRCodeHelper (310-351)
  • GetQRCode (325-334)
  • GetQRCode (343-350)
⏰ 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). (2)
  • GitHub Check: build
  • GitHub Check: additional-tests

@Shane32
Copy link
Owner Author

Shane32 commented Oct 5, 2025

For better readability of the GitHub diff, please enable 'hide whitespace'.
image

@Shane32 Shane32 requested a review from gfoidl October 5, 2025 05:20
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.

Really nice 👍🏻
And thanks for all the comments, to make it easier to understand and for better future maintenance.

Shane32 and others added 2 commits October 5, 2025 09:23
Co-authored-by: Günther Foidl <gue@korporal.at>
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 (1)
QRCoder/PdfByteQRCode.cs (1)

60-60: Verify the XML doc comment phrasing.

The comment states "obsolete, no longer used" but the parameter has not been marked with [Obsolete] attribute. Consider rephrasing to clarify that the parameter is retained for backward compatibility but is not used internally.

Suggested phrasing:

-    /// <param name="jpgQuality">The JPEG quality parameter (obsolete, no longer used).</param>
+    /// <param name="jpgQuality">This parameter is no longer used but is retained for backward compatibility.</param>
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6f27ee9 and c0a1991.

📒 Files selected for processing (5)
  • QRCoder/PdfByteQRCode.cs (1 hunks)
  • QRCoderApiTests/net60-windows/QRCoder.approved.txt (0 hunks)
  • QRCoderApiTests/net60/QRCoder.approved.txt (1 hunks)
  • QRCoderApiTests/netstandard13/QRCoder.approved.txt (1 hunks)
  • QRCoderTests/PdfByteQRCodeRendererTests.cs (1 hunks)
💤 Files with no reviewable changes (1)
  • QRCoderApiTests/net60-windows/QRCoder.approved.txt
🚧 Files skipped from review as they are similar to previous changes (2)
  • QRCoderApiTests/netstandard13/QRCoder.approved.txt
  • QRCoderTests/PdfByteQRCodeRendererTests.cs
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/PdfByteQRCode.cs (4)
QRCoder/PngByteQRCode.cs (3)
  • GetGraphic (37-49)
  • GetGraphic (60-61)
  • Dispose (176-180)
QRCoder/BitmapByteQRCode.cs (1)
  • HexColorToByteArray (143-151)
QRCoder/QRCodeData.cs (1)
  • Dispose (226-231)
QRCoder/AbstractQRCode.cs (1)
  • Dispose (42-46)
⏰ 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). (2)
  • GitHub Check: additional-tests
  • GitHub Check: build
🔇 Additional comments (10)
QRCoder/PdfByteQRCode.cs (10)

64-73: LGTM!

The dimension calculations and color parsing logic are correct. The conversion from DPI to PDF points (72 DPI standard) is handled appropriately.


75-96: LGTM!

The platform-conditional StreamWriter initialization correctly handles the differences between framework versions. The NETFRAMEWORK target doesn't have the leaveOpen parameter, so the writer is not explicitly disposed (lines 186-188), which is appropriate since closing it would also close the underlying stream before we can extract the bytes.


99-126: LGTM!

The PDF catalog and pages structure is correctly formed according to PDF specification. The inline page dictionary is a valid approach and the extensive comments are helpful for understanding the PDF structure.


128-140: LGTM!

The content stream construction is correct. The transformation matrix properly handles the Y-axis flip (PDF's origin is bottom-left, while the module matrix origin is top-left), and the drawing sequence (background fill, then dark modules fill) produces the correct result.


141-151: LGTM!

The content stream object is correctly structured with the required length dictionary entry and proper stream markers.


153-180: LGTM!

The cross-reference table and trailer are correctly formatted according to PDF specification. The checked cast on line 154 appropriately guards against overflow, and the xref entries use the required 10-digit zero-padded format.


184-191: LGTM!

The cleanup logic correctly handles the platform-specific StreamWriter disposal, and the stream remains accessible for the final ToArray() call.


199-219: LGTM!

The path creation logic correctly generates rectangle commands for each dark module. The implementation is straightforward and correct.


221-237: LGTM!

The color conversion correctly normalizes RGB byte values to the PDF color space (0.0-1.0 range). The implementation uses the multiplication optimization suggested in the past review comment, which improves performance compared to division. The input validation ensures the color array has exactly 3 bytes.

Based on learnings


239-251: LGTM!

The ToStr overloads correctly use InvariantCulture for consistent PDF formatting across different locales. The float format pattern "0.######" provides appropriate precision while avoiding unnecessary trailing zeros.

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 (2)
QRCoder/PdfByteQRCode.cs (2)

39-47: Consider extracting hex color parsing to a common utility.

This method is duplicated in BitmapByteQRCode.cs (lines 141-149). Extracting this logic to a shared helper class would improve maintainability and reduce code duplication.

Example:

+// In a shared utility class (e.g., ColorHelper.cs)
+internal static class ColorHelper
+{
+    internal static byte[] HexColorToByteArray(string colorString)
+    {
+        if (colorString.StartsWith("#"))
+            colorString = colorString.Substring(1);
+        byte[] byteColor = new byte[colorString.Length / 2];
+        for (int i = 0; i < byteColor.Length; i++)
+            byteColor[i] = byte.Parse(colorString.Substring(i * 2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+        return byteColor;
+    }
+}

Then use ColorHelper.HexColorToByteArray() in both classes.


150-163: Consider using long for xref positions.

The checked casts from long to int on lines 150 and 163 guard against overflow but will throw if the PDF exceeds ~2GB. While unlikely for QR codes, using long consistently would be more robust.

Apply this diff:

         writer.Flush();
-        var startxref = checked((int)stream.Position);
+        var startxref = stream.Position;
 
         // Cross-reference table - maps object numbers to byte offsets
         writer.Write(
             "xref\r\n" +                                   // Cross-reference table keyword
             "0 " + ToStr(xrefs.Count + 1) + "\r\n" +       // First object number (0) and count of entries
             "0000000000 65535 f\r\n"                       // Entry 0: always free, generation 65535, 'f' = free
         );
 
         // Write byte offset for each object
         foreach (var refValue in xrefs)
         {
             // Write each entry as a 10-digit zero-padded byte offset, 5-digit zero-padded generation number (0), and 'n' = in use
-            writer.Write(checked((int)refValue).ToString("0000000000", CultureInfo.InvariantCulture) + " 00000 n\r\n");
+            writer.Write(refValue.ToString("0000000000", CultureInfo.InvariantCulture) + " 00000 n\r\n");
         }

Note: The PDF format itself uses 10-digit fields for offsets, which can accommodate values up to 9,999,999,999 bytes (~9.3GB), so long is appropriate.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c0a1991 and 9aad36a.

📒 Files selected for processing (1)
  • QRCoder/PdfByteQRCode.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
QRCoder/PdfByteQRCode.cs (2)
QRCoder/PngByteQRCode.cs (3)
  • GetGraphic (35-47)
  • GetGraphic (58-59)
  • Dispose (174-178)
QRCoder/BitmapByteQRCode.cs (1)
  • HexColorToByteArray (142-150)
⏰ 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). (2)
  • GitHub Check: additional-tests
  • GitHub Check: build
🔇 Additional comments (7)
QRCoder/PdfByteQRCode.cs (7)

58-59: Backward compatibility approach is appropriate.

The jpgQuality parameter is retained but unused, which is clearly documented as obsolete in the XML comments. This approach maintains backward compatibility for the v1.x line while transitioning to the new rectangle-based rendering, as mentioned in the PR objectives.


82-147: Excellent PDF structure documentation.

The inline comments thoroughly explain each PDF command and object structure, making the code highly maintainable and educational. This is particularly valuable for a complex format like PDF.


195-215: LGTM!

The path generation logic is correct and efficient. Using StringBuilder for accumulating path commands is appropriate, and the PDF rectangle syntax (x y width height re) is correct.


222-233: Optimization from past review implemented correctly.

The use of const float inv255 = 1 / 255f; implements the performance optimization suggested in the past review by gfoidl. The color validation and conversion logic are correct.

Based on past review comment.


240-247: Correct use of invariant culture formatting.

Using CultureInfo.InvariantCulture for both int and float formatting ensures consistent PDF generation regardless of system locale, which is essential for PDF compatibility.


253-293: LGTM!

The helper methods follow proper resource management patterns with using statements and provide convenient static APIs for generating QR codes. The implementation is consistent with similar helpers in the codebase.


240-247: Float precision is sufficient; no changes needed.

The 6-decimal format ("0.######") covers all current uses (color components, coordinate transforms, page dimensions) with sub-point precision far beyond PDF requirements.

@Shane32 Shane32 merged commit c68ebce into master Oct 5, 2025
11 checks passed
@Shane32 Shane32 deleted the pdf branch October 5, 2025 14:27
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.

3 participants