using System.Buffers.Binary;
using System.IO.Compression;
using System.Text;
namespace Sunfish.Qr;
/// <summary>Salidas de un <see cref="QrCode"/>: PNG en blanco y negro y texto con medios bloques.</summary>
public static class QrImage
{
/// <summary>Margen claro alrededor que exige la norma (en modulos).</summary>
public const int QuietZone = 4;
/// <summary>PNG de 1 bit por pixel, <paramref name="scale"/> pixeles por modulo, con margen.</summary>
public static byte[] Png(QrCode qr, int scale = 6, int quiet = QuietZone)
{
int size = (qr.Size + quiet * 2) * scale;
int rowBytes = (size + 7) / 8;
var raw = new byte[(rowBytes + 1) * size];
for (int y = 0; y < size; y++)
{
int row = y * (rowBytes + 1); // el primer byte de cada fila es el filtro (0, ninguno)
for (int x = 0; x < size; x++)
{
int mx = x / scale - quiet, my = y / scale - quiet;
bool dark = mx >= 0 && my >= 0 && mx < qr.Size && my < qr.Size && qr[mx, my];
if (!dark) // 1 = blanco en escala de grises de 1 bit
raw[row + 1 + x / 8] |= (byte)(0x80 >> (x % 8));
}
}
var png = new MemoryStream();
png.Write([0x89, (byte)'P', (byte)'N', (byte)'G', 0x0D, 0x0A, 0x1A, 0x0A]);
var header = new byte[13];
BinaryPrimitives.WriteUInt32BigEndian(header, (uint)size);
BinaryPrimitives.WriteUInt32BigEndian(header.AsSpan(4), (uint)size);
header[8] = 1; // bits por pixel
header[9] = 0; // escala de grises
Chunk(png, "IHDR", header);
var compressed = new MemoryStream();
using (var z = new ZLibStream(compressed, CompressionLevel.Optimal, leaveOpen: true))
z.Write(raw);
Chunk(png, "IDAT", compressed.ToArray());
Chunk(png, "IEND", []);
return png.ToArray();
}
private static void Chunk(Stream output, string type, byte[] data)
{
Span<byte> length = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(length, (uint)data.Length);
output.Write(length);
var typeBytes = Encoding.ASCII.GetBytes(type);
output.Write(typeBytes);
output.Write(data);
uint crc = Web.Crc32.Update(Web.Crc32.Update(0, typeBytes), data);
Span<byte> crcBytes = stackalloc byte[4];
BinaryPrimitives.WriteUInt32BigEndian(crcBytes, crc);
output.Write(crcBytes);
}
/// <summary>
/// Filas de texto con medios bloques (dos modulos por caracter en vertical). true = oscuro;
/// quien lo pinta decide colores (la TUI pone fondo claro para que lo lean los moviles).
/// </summary>
public static IEnumerable<string> HalfBlocks(QrCode qr, int quiet = 2)
{
int size = qr.Size + quiet * 2;
bool Dark(int x, int y)
{
x -= quiet;
y -= quiet;
return x >= 0 && y >= 0 && x < qr.Size && y < qr.Size && qr[x, y];
}
for (int y = 0; y < size; y += 2)
{
var sb = new StringBuilder(size);
for (int x = 0; x < size; x++)
{
bool top = Dark(x, y), bottom = y + 1 < size && Dark(x, y + 1);
sb.Append((top, bottom) switch
{
(true, true) => '█',
(true, false) => '▀',
(false, true) => '▄',
_ => ' ',
});
}
yield return sb.ToString();
}
}
}