123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209 |
- using Terminal.Gui;
- namespace Terminal.Gui;
- /// <summary>
- /// Encodes a images into the sixel console image output format.
- /// </summary>
- public class SixelEncoder
- {
- /// <summary>
- /// Gets or sets the quantizer responsible for building a representative
- /// limited color palette for images and for mapping novel colors in
- /// images to their closest palette color
- /// </summary>
- public ColorQuantizer Quantizer { get; set; } = new ();
- /// <summary>
- /// Encode the given bitmap into sixel encoding
- /// </summary>
- /// <param name="pixels"></param>
- /// <returns></returns>
- public string EncodeSixel (Color [,] pixels)
- {
- const string start = "\u001bP"; // Start sixel sequence
- const string defaultRatios = "0;0;0"; // Defaults for aspect ratio and grid size
- const string completeStartSequence = "q"; // Signals beginning of sixel image data
- const string noScaling = "\"1;1;"; // no scaling factors (1x1);
- string fillArea = GetFillArea (pixels);
- string pallette = GetColorPalette (pixels );
- string pixelData = WriteSixel (pixels);
- const string terminator = "\u001b\\"; // End sixel sequence
- return start + defaultRatios + completeStartSequence + noScaling + fillArea + pallette + pixelData + terminator;
- }
- /*
- A sixel is a column of 6 pixels - with a width of 1 pixel
- Column controlled by one sixel character:
- [ ] - Bit 0 (top-most pixel)
- [ ] - Bit 1
- [ ] - Bit 2
- [ ] - Bit 3
- [ ] - Bit 4
- [ ] - Bit 5 (bottom-most pixel)
- */
- /**
- * This method is adapted from
- * https://github.com/jerch/node-sixel/
- *
- * Copyright (c) 2019 Joerg Breitbart.
- * @license MIT
- */
- private string WriteSixel (Color [,] pixels)
- {
- StringBuilder sb = new StringBuilder ();
- int height = pixels.GetLength (1);
- int width = pixels.GetLength (0);
- int n = 1; // Used for checking when to add the line terminator
- // Iterate over each row of the image
- for (int y = 0; y < height; y += 6)
- {
- sb.Append (ProcessBand (pixels, y, Math.Min (6, height - y), width));
- // Line separator between bands
- if (y + 6 < height) // Only add separator if not the last band
- {
- sb.Append ("-");
- }
- }
- return sb.ToString ();
- }
- private string ProcessBand (Color [,] pixels, int startY, int bandHeight, int width)
- {
- var last = new sbyte [Quantizer.Palette.Count + 1];
- var code = new byte [Quantizer.Palette.Count + 1];
- var accu = new ushort [Quantizer.Palette.Count + 1];
- var slots = new short [Quantizer.Palette.Count + 1];
- Array.Fill (last, (sbyte)-1);
- Array.Fill (accu, (ushort)1);
- Array.Fill (slots, (short)-1);
- var usedColorIdx = new List<int> ();
- var targets = new List<List<string>> ();
- // Process columns within the band
- for (int x = 0; x < width; ++x)
- {
- Array.Clear (code, 0, usedColorIdx.Count);
- // Process each row in the 6-pixel high band
- for (int row = 0; row < bandHeight; ++row)
- {
- var color = pixels [x, startY + row];
- int colorIndex = Quantizer.GetNearestColor (color);
- if (slots [colorIndex] == -1)
- {
- targets.Add (new List<string> ());
- if (x > 0)
- {
- last [usedColorIdx.Count] = 0;
- accu [usedColorIdx.Count] = (ushort)x;
- }
- slots [colorIndex] = (short)usedColorIdx.Count;
- usedColorIdx.Add (colorIndex);
- }
- code [slots [colorIndex]] |= (byte)(1 << row); // Accumulate SIXEL data
- }
- // Handle transitions between columns
- for (int j = 0; j < usedColorIdx.Count; ++j)
- {
- if (code [j] == last [j])
- {
- accu [j]++;
- }
- else
- {
- if (last [j] != -1)
- {
- targets [j].Add (CodeToSixel (last [j], accu [j]));
- }
- last [j] = (sbyte)code [j];
- accu [j] = 1;
- }
- }
- }
- // Process remaining data for this band
- for (int j = 0; j < usedColorIdx.Count; ++j)
- {
- if (last [j] != 0)
- {
- targets [j].Add (CodeToSixel (last [j], accu [j]));
- }
- }
- // Build the final output for this band
- var result = new StringBuilder ();
- for (int j = 0; j < usedColorIdx.Count; ++j)
- {
- result.Append ($"#{usedColorIdx [j]}{string.Join ("", targets [j])}$");
- }
- return result.ToString ();
- }
- private static string CodeToSixel (int code, int repeat)
- {
- char c = (char)(code + 63);
- if (repeat > 3) return "!" + repeat + c;
- if (repeat == 3) return c.ToString () + c + c;
- if (repeat == 2) return c.ToString () + c;
- return c.ToString ();
- }
- private string GetColorPalette (Color [,] pixels)
- {
- Quantizer.BuildPalette (pixels);
- StringBuilder paletteSb = new StringBuilder ();
- for (int i = 0; i < Quantizer.Palette.Count; i++)
- {
- var color = Quantizer.Palette.ElementAt (i);
- paletteSb.AppendFormat ("#{0};2;{1};{2};{3}",
- i,
- color.R * 100 / 255,
- color.G * 100 / 255,
- color.B * 100 / 255);
- }
- return paletteSb.ToString ();
- }
- private string GetFillArea (Color [,] pixels)
- {
- int widthInChars = GetWidthInChars (pixels);
- int heightInChars = GetHeightInChars (pixels);
- return $"{widthInChars};{heightInChars}";
- }
- private int GetHeightInChars (Color [,] pixels)
- {
- int height = pixels.GetLength (1);
- return (height + 5) / 6;
- }
- private int GetWidthInChars (Color [,] pixels)
- {
- return pixels.GetLength (0);
- }
- }
|