Skip to content

Commit

Permalink
Added ToGraphicsPath()
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelbl committed Jul 3, 2022
1 parent 9059f86 commit 816624d
Show file tree
Hide file tree
Showing 4 changed files with 137 additions and 14 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,6 @@

# Visual Studio Code
.vscode/

# Rider
.idea/
136 changes: 123 additions & 13 deletions QrCodeGenerator/QrCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -404,28 +404,138 @@ public string ToSvgString(int border, string foreground, string background)
.Append($"\t<rect width=\"100%\" height=\"100%\" fill=\"{background}\"/>\n")
.Append("\t<path d=\"");

for (int y = 0; y < Size; y++)
// Work on copy as it is destructive
var modules = CopyModules();
CreatePath(sb, modules, border);

return sb
.Append($"\" fill=\"{foreground}\"/>\n")
.Append("</svg>\n")
.ToString();
}

/// <summary>
/// Creates a graphics of this QR code valid in SVG or XAML.
/// <para>
/// The graphics path uses a coordinate system where each module is 1 unit wide and tall,
/// and the top left module is offset vertically and horizontally by <i>border</i> units.
/// </para>
/// <para>
/// Note that a border width other than 0 only make sense if the bounding box of the QR code
/// is explicitly set by the graphics using this path. If the bounding box of this path is
/// automatically derived, at least the right and bottom border will be missing.
/// </para>
/// <para>
/// The path will look like this: <c>M3,3h7v1h-7z M12,3h1v4h-1z ... M70,71h1v1h-1z</c>
/// </para>
/// </summary>
/// <param name="border">The border width, as a factor of the module (QR code pixel) size</param>
/// <returns>The graphics path</returns>
/// <exception cref="ArgumentOutOfRangeException">Thrown if border is negative</exception>
public string ToGraphicsPath(int border = 0)
{
if (border < 0)
{
for (int x = 0; x < Size; x++)
throw new ArgumentOutOfRangeException(nameof(border), "Border must be non-negative");
}

// Work on copy as it is destructive
var modules = CopyModules();
var path = new StringBuilder();
CreatePath(path, modules, border);
return path.ToString();
}

#endregion


#region Graphics path


// Append a SVG/XAML path for the QR code to the provided string builder
private static void CreatePath(StringBuilder path, bool[,] modules, int border)
{
// Simple algorithms to reduce the number of rectangles for drawing the QR code
// and reduce SVG/XAML size.
var size = modules.GetLength(0);
for (var y = 0; y < size; y++)
{
for (var x = 0; x < size; x++)
{
if (!GetModule(x, y))
if (modules[y, x])
{
continue;
DrawLargestRectangle(path, modules, x, y, border);
}
}
}
}

// Find, draw and clear largest rectangle with (x, y) as the top left corner
private static void DrawLargestRectangle(StringBuilder path, bool[,] modules, int x, int y, int border)
{
var size = modules.GetLength(0);

if (x != 0 || y != 0)
{
sb.Append(" ");
}
var bestW = 1;
var bestH = 1;
var maxArea = 1;

sb.Append($"M{x + border},{y + border}h1v1h-1z");
var xLimit = size;
var iy = y;
while (iy < size && modules[iy, x])
{
var w = 0;
while (x + w < xLimit && modules[iy, x + w])
{
w++;
}

var area = w * (iy - y + 1);
if (area > maxArea)
{
maxArea = area;
bestW = w;
bestH = iy - y + 1;
}
xLimit = x + w;
iy++;
}

return sb
.Append($"\" fill=\"{foreground}\"/>\n")
.Append("</svg>\n")
.ToString();
// append path command
if (x != 0 || y != 0)
{
path.Append(" ");
}
path.Append($"M{x + border},{y + border}h{bestW}v{bestH}h{-bestW}z");

// clear processed modules
ClearRectangle(modules, x, y, bestW, bestH);
}

// Clear a rectangle of modules
private static void ClearRectangle(bool[,] modules, int x, int y, int width, int height)
{
for (var iy = y; iy < y + height; iy++)
{
for (var ix = x; ix < x + width; ix++)
{
modules[iy, ix] = false;
}
}
}

// Create a copy of the modules
private bool[,] CopyModules()
{
var modules = new bool[Size, Size];
for (var y = 0; y < Size; y++)
{
for (var x = 0; x < Size; x++)
{
modules[y, x] = GetModule(x, y);
}
}

return modules;
}

#endregion
Expand Down
2 changes: 1 addition & 1 deletion QrCodeGenerator/QrCodeGenerator.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ Optional advanced features:
<PropertyGroup Label="Packaging">
<DebugType>embedded</DebugType>
<EmbedAllSources>true</EmbedAllSources>
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<ContinuousIntegrationBuild Condition="$(Configuration) == 'Release'">true</ContinuousIntegrationBuild>
</PropertyGroup>

<ItemGroup>
Expand Down
10 changes: 10 additions & 0 deletions QrCodeGeneratorTest/SvgTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,5 +59,15 @@ public void SvgImageWithColor()

Assert.DoesNotContain("#FFFFFF", svg);
}

[Fact]
public void SvgPath()
{
var qrCode = EncodeText(CODE_TEXT, Ecc.Medium);
var path = qrCode.ToGraphicsPath(3);

Assert.StartsWith("M3,3h", path);
Assert.EndsWith("h1v1h-1z", path);
}
}
}

0 comments on commit 816624d

Please sign in to comment.