Editor/HumanoidMocap/PhoneQr/Graphics.cs
/*
 * QR code generator library (.NET)
 *
 * Copyright (c) Manuel Bleichenbacher (MIT License)
 * https://github.com/manuelbl/QrCodeGenerator
 * Copyright (c) Project Nayuki (MIT License)
 * https://www.nayuki.io/page/qr-code-generator-library
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */

using System;
using System.Globalization;
using System.Text;

namespace HumanoidMocap.Editor.PhoneQr
{
    internal class Graphics
    {
        internal Graphics(int size, bool[,] modules)
        {
            _size = size;
            _modules = modules;
        }

        private readonly int _size;

        // The modules of this QR code (false = light, true = dark).
        // Immutable after constructor finishes.
        private readonly bool[,] _modules;

        internal string ToSvgString(int border, string foreground, string background)
        {
            if (border < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(border), "Border must be non-negative");
            }

            var dim = _size + border * 2;
            var sb = new StringBuilder()
                .Append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n")
                .Append("<!DOCTYPE svg PUBLIC \"-//W3C//DTD SVG 1.1//EN\" \"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd\">\n")
                .Append($"<svg xmlns=\"http://www.w3.org/2000/svg\" version=\"1.1\" viewBox=\"0 0 {dim} {dim}\" stroke=\"none\">\n")
                .Append($"\t<rect width=\"100%\" height=\"100%\" fill=\"{background}\"/>\n")
                .Append("\t<path d=\"");

            // Work on copy as it is destructive
            var modules = CopyModules();
            CreatePath(sb, modules, border);

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

        internal string ToGraphicsPath(int border)
        {
            if (border < 0)
            {
                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();
        }

        internal byte[] ToBmpBitmap(int border, int scale, int foreground, int background)
        {
            if (scale < 1)
            {
                throw new ArgumentOutOfRangeException(nameof(scale), scale, "Scale must be greater than 0.");
            }

            if (border < 0)
            {
                throw new ArgumentOutOfRangeException(nameof(border), border, "Border must be non-negative.");
            }

            var dim = (_size + 2 * border) * scale;

            if (dim > short.MaxValue)
            {
                throw new ArgumentOutOfRangeException(nameof(scale), "Scale or border too large.");
            }

            // NOTE: Works for Size > 0
            // Modules to bytes
            // x >> 3 == x / 8
            var bytesToWrite = ((dim - 1) >> 3) + 1;

            // NOTE: Align to 4 bytes
            // This is a Bitmap requirement
            // (size + (align - 1)) & ~(align - 1)
            var aligned = (bytesToWrite + 3) & ~3;
            var fileSize = 62 + dim * aligned;

            var buf = new byte[fileSize];

            // NOTE: BMP file header
            buf[0] = (byte)'B';
            buf[1] = (byte)'M';

            buf[2] = (byte)fileSize;
            buf[3] = (byte)(fileSize >> 8);
            buf[4] = (byte)(fileSize >> 16);
            buf[5] = (byte)(fileSize >> 24);

            // NOTE: Offset to bitmap data
            buf[10] = 62;

            // NOTE: BMP info header
            buf[14] = 40;

            // NOTE: Image width
            buf[18] = (byte)dim;
            buf[19] = (byte)(dim >> 8);
            buf[20] = (byte)(dim >> 16);
            buf[21] = (byte)(dim >> 24);

            // NOTE: Image height
            buf[22] = buf[18];
            buf[23] = buf[19];
            buf[24] = buf[20];
            buf[25] = buf[21];

            // NOTE: Number of color planes (usually 1)
            // Must be non-zero
            buf[26] = 1;

            // NOTE: Number of bits per pixel (1 bpp)
            buf[28] = 1;

            // NOTE: Horizontal resolution (pixels/meter)
            // 3780 ppm (96 dpi)
            buf[38] = 196;
            buf[39] = 14;

            // NOTE: Vertical resolution (pixels/meter)
            // 3780 ppm (96 dpi)
            buf[42] = buf[38];
            buf[43] = buf[39];

            // NOTE: Color table
            // Alpha isn't useful here
            // Foreground - Dark
            buf[54] = (byte)foreground; // blue
            buf[55] = (byte)(foreground >> 8); // green
            buf[56] = (byte)(foreground >> 16); // red

            // Background - Light
            buf[58] = (byte)background; // blue
            buf[59] = (byte)(background >> 8); // green
            buf[60] = (byte)(background >> 16); // red;

            var scaledBorder = border * scale;

            int i;
            int y;
            byte px;

            if (border > 0)
            {
                var scaledSize = _size * scale;

                for (i = 0; i < aligned; ++i)
                {
                    px = 255;

                    if (i == bytesToWrite - 1)
                    {
                        px = (byte)(255 << ((bytesToWrite << 3) - dim));
                    }
                    else if (i >= bytesToWrite)
                    {
                        px = 0;
                    }

                    for (y = 0; y < scaledBorder; ++y)
                    {
                        buf[62 + i + y * aligned] = px;
                        buf[62 + i + (y + scaledSize + scaledBorder) * aligned] = px;
                    }
                }
            }

            for (y = 0; y < _size; ++y)
            {
                int j;
                var yOffset = y * scale + scaledBorder;

                for (i = 0; i < aligned; ++i)
                {
                    px = 0;

                    for (j = 0; j < 8; ++j)
                    {
                        var x = ((i << 3) + j) / scale;

                        if (x >= dim)
                        {
                            continue;
                        }

                        if (x < border || x >= _size + border)
                        {
                            px |= (byte)(1 << (7 - j));
                            continue;
                        }

                        px |= (byte)(_modules[(_size - y - 1), x - border] ? 0 : 1 << (7 - j));
                    }

                    buf[62 + i + yOffset * aligned] = px;
                }

                // NOTE: Copy rows when scaling
                for (i = 1; i <= scale - 1; ++i)
                {
                    for (j = 0; j < aligned; ++j)
                    {
                        buf[62 + j + (yOffset + i) * aligned] = buf[62 + j + yOffset * aligned];
                    }
                }
            }

            return buf;
        }

        // Append a SVG/XAML path for the QR code to the provided string builder
        private  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 (modules[y, x])
                    {
                        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);

            var bestW = 1;
            var bestH = 1;
            var maxArea = 1;

            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++;
            }

            // append path command
            if (x != 0 || y != 0)
            {
                path.Append(" ");
            }

            // Different locales use different minus signs.
            FormattableString pathElement = $"M{x + border},{y + border}h{bestW}v{bestH}h{-bestW}z";
            path.Append(pathElement.ToString(CultureInfo.InvariantCulture));

            // 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 (in row-major order)
        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] = _modules[y, x];
                }
            }

            return modules;
        }
    }
}