From 87af541cc4df90db56e3bb133fbca59d13da40ea Mon Sep 17 00:00:00 2001 From: Alek Ratzloff Date: Thu, 30 May 2024 12:22:58 -0700 Subject: [PATCH] Remove fullsource and use the current (previous) commit hash instead Regenerating the fullsource hash was a bit tedious. Instead, it makes a bit more sense to use the current commit hash as the header image. This uses sha1 nibbleart. Signed-off-by: Alek Ratzloff --- README.md | 2 +- examples/commithash.svg | 44 ++ examples/fullsource-md5-nibble.svg | 34 - examples/fullsource-md5-randomart.svg | 44 -- examples/fullsource-sha1-nibble.svg | 42 -- examples/fullsource-sha1-randomart.svg | 44 -- examples/fullsource-sha224-nibble.svg | 58 -- examples/fullsource-sha224-randomart.svg | 58 -- examples/fullsource-sha256-nibble.svg | 66 -- examples/fullsource-sha256-randomart.svg | 58 -- examples/fullsource-sha384-nibble.svg | 98 --- examples/fullsource-sha384-randomart.svg | 112 ---- examples/fullsource-sha512-nibble.svg | 130 ---- examples/fullsource-sha512-randomart.svg | 112 ---- examples/fullsource.in | 757 ----------------------- tools/genexamples.sh | 7 +- 16 files changed, 49 insertions(+), 1617 deletions(-) create mode 100644 examples/commithash.svg delete mode 100644 examples/fullsource-md5-nibble.svg delete mode 100644 examples/fullsource-md5-randomart.svg delete mode 100644 examples/fullsource-sha1-nibble.svg delete mode 100644 examples/fullsource-sha1-randomart.svg delete mode 100644 examples/fullsource-sha224-nibble.svg delete mode 100644 examples/fullsource-sha224-randomart.svg delete mode 100644 examples/fullsource-sha256-nibble.svg delete mode 100644 examples/fullsource-sha256-randomart.svg delete mode 100644 examples/fullsource-sha384-nibble.svg delete mode 100644 examples/fullsource-sha384-randomart.svg delete mode 100644 examples/fullsource-sha512-nibble.svg delete mode 100644 examples/fullsource-sha512-randomart.svg delete mode 100644 examples/fullsource.in diff --git a/README.md b/README.md index 3641ae8..70b9683 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # colorhash -![Hash of the full colorhash source code](examples/fullsource-sha512-nibble.svg) +![Hash of the previous commit hash](examples/commithash.svg) A tool for creating distinct art based on input data or hash. diff --git a/examples/commithash.svg b/examples/commithash.svg new file mode 100644 index 0000000..cc5b131 --- /dev/null +++ b/examples/commithash.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/examples/fullsource-md5-nibble.svg b/examples/fullsource-md5-nibble.svg deleted file mode 100644 index a9059b0..0000000 --- a/examples/fullsource-md5-nibble.svg +++ /dev/null @@ -1,34 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-md5-randomart.svg b/examples/fullsource-md5-randomart.svg deleted file mode 100644 index 985a5a0..0000000 --- a/examples/fullsource-md5-randomart.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha1-nibble.svg b/examples/fullsource-sha1-nibble.svg deleted file mode 100644 index 621ebb5..0000000 --- a/examples/fullsource-sha1-nibble.svg +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha1-randomart.svg b/examples/fullsource-sha1-randomart.svg deleted file mode 100644 index a890533..0000000 --- a/examples/fullsource-sha1-randomart.svg +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha224-nibble.svg b/examples/fullsource-sha224-nibble.svg deleted file mode 100644 index 60d5e84..0000000 --- a/examples/fullsource-sha224-nibble.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha224-randomart.svg b/examples/fullsource-sha224-randomart.svg deleted file mode 100644 index bc04e9f..0000000 --- a/examples/fullsource-sha224-randomart.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha256-nibble.svg b/examples/fullsource-sha256-nibble.svg deleted file mode 100644 index ff458cc..0000000 --- a/examples/fullsource-sha256-nibble.svg +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha256-randomart.svg b/examples/fullsource-sha256-randomart.svg deleted file mode 100644 index eff6163..0000000 --- a/examples/fullsource-sha256-randomart.svg +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha384-nibble.svg b/examples/fullsource-sha384-nibble.svg deleted file mode 100644 index b7dc7ff..0000000 --- a/examples/fullsource-sha384-nibble.svg +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha384-randomart.svg b/examples/fullsource-sha384-randomart.svg deleted file mode 100644 index 5072200..0000000 --- a/examples/fullsource-sha384-randomart.svg +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha512-nibble.svg b/examples/fullsource-sha512-nibble.svg deleted file mode 100644 index 10acdcc..0000000 --- a/examples/fullsource-sha512-nibble.svg +++ /dev/null @@ -1,130 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource-sha512-randomart.svg b/examples/fullsource-sha512-randomart.svg deleted file mode 100644 index 3136ea4..0000000 --- a/examples/fullsource-sha512-randomart.svg +++ /dev/null @@ -1,112 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/examples/fullsource.in b/examples/fullsource.in deleted file mode 100644 index c68e132..0000000 --- a/examples/fullsource.in +++ /dev/null @@ -1,757 +0,0 @@ -"Generate a graphic based on the hash of an input file." -from .cli import cli_main - - -if __name__ == '__main__': - cli_main() -"All things that turn a numeric matrix into a colored matrix." -import abc -from typing import Sequence - -from .color import Color -from .matricizer import Matrix -from .palettes import Palette - - -ColorMatrix = Sequence[Sequence[Color]] - - -class Colorizer(metaclass=abc.ABCMeta): - """ - The base Colorizer class. - - A colorizer turns a numeric matrix into a color matrix, colors being represented by strings of - HTML colors. - """ - - @abc.abstractmethod - def colorize(self, matrix: Matrix) -> ColorMatrix: - """ - Colorize a matrix. - - :param matrix: the matrix to colorize. - :returns: the colorized matrix. - """ - - -class PaletteColorizer(Colorizer): - """ - A palette colorizer. - - This colorizer will use a palette to colorize its inputs. A palette is 16 colors. - """ - def __init__(self, palette: Palette) -> None: - """ - Create a new palette colorizer for a given palette. - - :param palette: the palette to use for this colorizer. - """ - self.palette = palette - - def colorize(self, matrix: Matrix) -> ColorMatrix: - """ - Colorize the given matrix using this colorizer's palette. - - :param matrix: the matrix to colorize. - :returns: the colorized matrix. - """ - return [[self.palette[v] for v in row] for row in matrix] -"All things that turn a hash into a matrix." -import abc -from typing import Mapping, Sequence - -from .palettes import Palette, DEFAULT_PALETTES, GRADIENT_PALETTES, MULTICOLOR_PALETTES - - -Matrix = Sequence[Sequence[int]] - - -class Matricizer(metaclass=abc.ABCMeta): - """ - The base Matricizer class. - - A matricizer turns a collection of hash bytes into a matrix of values between 0x0 and 0xf - (inclusive). The method by which this is done is up to the matricizer. - """ - - def __init__(self, w: int, h: int) -> None: - """ - Create a new matricizer for the given dimensions. - - :param w: the width of the output matrix. - :param h: the height of hte output matrix. - """ - self.w = w - self.h = h - - @abc.abstractmethod - def matricize(self, data: bytes) -> Matrix: - """ - Convert a hash to a matrix of given width and height. - - :param data: the hash data to turn into a matrix. - :returns: the matrix converted from the hash data. - """ - - @staticmethod - @abc.abstractmethod - def choose_dimensions(hash: str) -> tuple[int, int]: - """ - Choose the dimensions for this matrix based on the hash algorithm. - - :param hash: the hash algorithm being used. - :returns: a width and height as a tuple. - """ - - def choose_palette( - self, data: bytes, palettes: Mapping[str, Palette] | None = None - ) -> Palette: - """ - Choose a palette based on the give data and palettes. - - By default, this method will choose the Nth palette from the sum of the data mod the length - of all palettes provided (using all palettes as the default). - """ - if palettes is None: - palettes = DEFAULT_PALETTES - return list(palettes.values())[sum(data) % len(palettes)] - - -class NibbleMatricizer(Matricizer): - """ - A matricizer that converts a hash based on all of the nibbles in the hash. - - While dimensions are not enforced by this matricizer, it is strongly recommended to use the - dimensions provided by the `NibbleMatricizer.DIMENSIONS` member. - """ - - DIMENSIONS = { - "md5": (8, 4), - "sha1": (8, 5), - "sha224": (8, 7), - "sha256": (8, 8), - "sha384": (12, 8), - "sha512": (16, 8), - } - - def matricize(self, data: bytes) -> Matrix: - """ - Convert a set of bytes to a list of rows of nibbles. - - :param data: the hash data to turn into a matrix. - :returns: the matrix converted from the hash data. - """ - - nibbles = [] - for b in data: - top = (b & 0xF0) >> 4 - bottom = b & 0x0F - nibbles += [top, bottom] - - if len(nibbles) != self.w * self.h: - raise ValueError( - f"input data length ({len(nibbles)}) must match matrix dimensions " - f"({self.w}x{self.h} = {self.w * self.h})" - ) - - cols = [] - row = [] - for b in nibbles: - row += [b] - if len(row) == self.w: - cols += [row] - row = [] - - return cols - - @staticmethod - def choose_dimensions(hash: str) -> tuple[int, int]: - return NibbleMatricizer.DIMENSIONS[hash] - - def choose_palette( - self, data: bytes, palettes: Mapping[str, Palette] | None = None - ) -> Palette: - return super().choose_palette(data, palettes or GRADIENT_PALETTES) - - -class RandomartMatricizer(Matricizer): - """ - A matricizer that converts hash data into a matrix based on the "randomart" algorithm from - ssh-keygen. - - See: https://github.com/openssh/openssh-portable/blob/fc5dc092830de23767c6ef67baa18310a64ee533/sshkey.c#L1014 - """ - - DIMENSIONS = { - "md5": (7, 6), - "sha1": (7, 6), - "sha224": (8, 7), - "sha256": (8, 7), - "sha384": (11, 10), - "sha512": (11, 10), - } - - def matricize(self, data: bytes) -> Matrix: - """ - Create a matrix based on the "randomart" algorithm from ssh-keygen. - - The algorithm is as follows: - - 1. Choose the point in the middle of the matrix. - 2. Iterate through the data, two bits at a time. - 3. If the low bit is set, then move the pointer right. Otherwise, move left. - 4. If the high bit is set, then move the pointer down. Otherwise, move up. - 5. If stepping in either direction would move us outside of the matrix, then don't move in - that direction. - 6. At the end of each step, increment the value in the matrix by one. - - :param data: the hash data to turn into a matrix. - :returns: the matrix converted from the hash data. - """ - - rows = [[0] * self.w for _ in range(self.h)] - c = self.w // 2 - r = self.h // 2 - for value in data: - for _ in range(4): - if value & 0x1: - c += 1 - else: - c -= 1 - if value & 0x2: - r += 1 - else: - r -= 1 - c = min(max(c, 0), self.w - 1) - r = min(max(r, 0), self.h - 1) - # max value is 0xf - if rows[r][c] < 0xF: - rows[r][c] += 1 - return rows - - @staticmethod - def choose_dimensions(hash: str) -> tuple[int, int]: - return RandomartMatricizer.DIMENSIONS[hash] - - def choose_palette( - self, data: bytes, palettes: Mapping[str, Palette] | None = None - ) -> Palette: - return super().choose_palette(data, palettes or MULTICOLOR_PALETTES) -"Base color palette definitions." -import abc -from typing import Sequence - -from .color import Color, HSLColor - - -class Palette(metaclass=abc.ABCMeta): - """ - A 16-color palette. - - All colors must be a `colorhash.Color`. - """ - - @abc.abstractmethod - def choose(self, color: int) -> Color: - """ - Chooses the given color in this palette. - """ - - def __getitem__(self, color: int) -> Color: - return self.choose(color) - - -class StaticPalette(Palette): - """ - A static color palette with discrete colors. - """ - - def __init__(self, colors: Sequence[Color]) -> None: - """ - Creates a new static color palette. - - :param colors: the colors for this palette. Must be exactly 16 colors. - """ - if len(colors) != 16: - raise ValueError(f"palette must have exactly 16 colors (got {len(colors)})") - self.colors = colors - - def choose(self, color: int) -> Color: - if not isinstance(color, int): - raise KeyError("palette color indices must be an integer") - return self.colors[color] - - -HSLRange = range | float | int | list[float | int] - - -def quantize(r: range, steps: int = 16) -> list[float]: - """ - Given a range and a number of steps, create a list of numbers starting and ending in the range - (inclusive) with that number of steps. - - :param r: the range to quantize. - :param steps: the number of steps to use. - :returns: a list of the quantized range. - """ - dist = abs(r.stop - r.start) - return [r.start + (i * dist / (steps - 1)) for i in range(steps)] - - -def hsl_colors(hue: HSLRange, sat: HSLRange, light: HSLRange) -> list[HSLColor]: - """ - Utility method to create 16 colors using HSL. - - :param hue: the hue, or range of hues, to use for this palette. - :param sat: the saturation, or range of saturations, to use for this palette. - :param light: the light value, or range of light values, to use for this palette. - """ - - if isinstance(hue, (float, int)): - hue = [hue] * 16 - elif isinstance(hue, range): - hue = quantize(hue) - assert len(hue) == 16, "hue values must be a list of 16 elements" - - if isinstance(sat, (float, int)): - sat = [sat] * 16 - elif isinstance(sat, range): - sat = quantize(sat) - assert len(sat) == 16, "saturation values must be a list of 16 elements" - - if isinstance(light, (float, int)): - light = [light] * 16 - elif isinstance(light, range): - light = quantize(light) - assert len(light) == 16, "light values must be a list of 16 elements" - - return [HSLColor(round(h), round(s), round(l)) for h, s, l in zip(hue, sat, light)] - - -GRADIENT_PALETTES = { - # Interesting thing with human perception. - # Between red and yellow, we can perceive "orange". We have a name for it and see it as a - # distinct color. However, between yellow and green, we see a sickly green; between green and - # cyan, a seafoam green; between cyan and blue, a lighter blue. - # - # Beside these, I think that between blue and magenta gives a color you could safely call - # "purple", and between magenta and red, you get a color you could safely call "pink". It seems - # that reds are more distinct to the human eye. - # - # For this reason, I have decided to pick these palettes as the "defaults", with a "dark" and - # "light" variant of each (lightness 0-50%, and 50-100% respectively), with an additional - # fully-saturated "rainbow" palette with all of the colors: - # - # red, orange, yellow, green, cyan, blue, purple, magenta, pink, gray, rainbow - # - # Also disabling yellow-light, that one just gives me a headache. It's hard to look at. - - "red-light": StaticPalette(hsl_colors(0, 100, range(50, 100))), - "red-dark": StaticPalette(hsl_colors(0, 100, range(0, 50))), - - "orange-light": StaticPalette(hsl_colors(30, 100, range(50, 100))), - "orange-dark": StaticPalette(hsl_colors(30, 100, range(0, 50))), - - #"yellow-light": StaticPalette(hsl_colors(60, 100, range(50, 100))), - "yellow-dark": StaticPalette(hsl_colors(60, 100, range(0, 50))), - - #"lime-light": StaticPalette(hsl_colors(90, 100, range(50, 100))), - #"lime-dark": StaticPalette(hsl_colors(90, 100, range(0, 50))), - - "green-light": StaticPalette(hsl_colors(120, 100, range(50, 100))), - "green-dark": StaticPalette(hsl_colors(120, 100, range(0, 50))), - - #"seafoam-light": StaticPalette(hsl_colors(150, 100, range(50, 100))), - #"seafoam-dark": StaticPalette(hsl_colors(150, 100, range(0, 50))), - - "cyan-light": StaticPalette(hsl_colors(180, 100, range(50, 100))), - "cyan-dark": StaticPalette(hsl_colors(180, 100, range(0, 50))), - - #"teal-light": StaticPalette(hsl_colors(210, 100, range(50, 100))), - #"teal-dark": StaticPalette(hsl_colors(210, 100, range(0, 50))), - - "blue-light": StaticPalette(hsl_colors(240, 100, range(50, 100))), - "blue-dark": StaticPalette(hsl_colors(240, 100, range(0, 50))), - - "purple-light": StaticPalette(hsl_colors(270, 100, range(50, 100))), - "purple-dark": StaticPalette(hsl_colors(270, 100, range(0, 50))), - - "magenta-light": StaticPalette(hsl_colors(300, 100, range(50, 100))), - "magenta-dark": StaticPalette(hsl_colors(300, 100, range(0, 50))), - - "pink-light": StaticPalette(hsl_colors(330, 100, range(50, 100))), - "pink-dark": StaticPalette(hsl_colors(330, 100, range(0, 50))), - - "gray-light": StaticPalette(hsl_colors(0, 0, range(50, 100))), - "gray-dark": StaticPalette(hsl_colors(0, 0, range(0, 50))), -} - - -MULTICOLOR_PALETTES = { - "rainbow": StaticPalette(hsl_colors(range(0, 360), 100, 50)), -} - -DEFAULT_PALETTES = { - **GRADIENT_PALETTES, **MULTICOLOR_PALETTES, -} - - -PALETTES = {**DEFAULT_PALETTES} -import abc -import colorsys -import dataclasses - - -class Color(metaclass=abc.ABCMeta): - """ - An abstract color class. - - This can be used to convert any color format into another color format. - """ - - def to_html_color(self) -> str: - """ - Convert this color to an HTML color. - - This may produce unexpected results if you are expecting an RGB color. If you are expecting - RGB, then you should use `color.to_rgb().to_html_color()` instead. - """ - - @abc.abstractmethod - def to_rgb(self) -> "RGBColor": - "Convert this color into an RGB color." - - @abc.abstractmethod - def to_hsl(self) -> "HSLColor": - "Convert this color into an HSL color." - - -@dataclasses.dataclass -class RGBColor(Color): - """ - An RGB color. Colors are expected to be a floating point value from [0.0-255.0). - """ - r: float - g: float - b: float - - def to_html_color(self) -> str: - r, g, b = round(self.r), round(self.g), round(self.b) - return f"#{r:02x}{g:02x}{b:02x}" - - def to_rgb(self) -> "RGBColor": - return self - - def to_hsl(self) -> "HSLColor": - r = self.r / 255.0 - g = self.g / 255.0 - b = self.b / 255.0 - h, l, s = colorsys.rgb_to_hls(r, g, b) - return HSLColor(h * 360.0, s * 100.0, l * 100.0) - - -@dataclasses.dataclass -class HSLColor(Color): - h: float - s: float - l: float - - def to_html_color(self) -> str: - return f"hsl({self.h:.02f},{self.s:.02f}%,{self.l:.02f}%)" - - def to_rgb(self) -> "RGBColor": - h = self.h / 360.0 - s = self.s / 100.0 - l = self.l / 100.0 - r, g, b = colorsys.hls_to_rgb(h, l, s) - return RGBColor(r * 255.0, g * 255.0, b * 255.0) - - - def to_hsl(self) -> "HSLColor": - return self -import abc - -from .color import Color -from .colorizer import ColorMatrix - - -class Writer(metaclass=abc.ABCMeta): - """ - Base writer class. - - This is used to write an input colorized matrix to a string, which is then forwarded to the - appropriate output. - """ - - @abc.abstractmethod - def write(self, matrix: ColorMatrix) -> str: - "Write the color matrix to a string." - - -class ANSIWriter(Writer): - def write(self, matrix: ColorMatrix) -> str: - ESC = '\x1b' - RESET = f"{ESC}[0m" - C = "██" - def ansi_color(c: Color) -> str: - c = c.to_rgb() - return f"{ESC}[38;2;{round(c.r)};{round(c.g)};{round(c.b)}m" - out = '' - for row in matrix: - for col in row: - out += ansi_color(col) - out += C - out += "\n" - out += RESET - return out - - -class SVGWriter(Writer): - def __init__(self, square_size: int) -> None: - """ - Create a new SVG writer that uses the given square size. - - :param square_size: the size of the squares generated, in pixels. - """ - self.square_size = square_size - - def write(self, matrix: ColorMatrix) -> str: - """ - Generate an SVG based on a given matrix. - - :param matrix: the color matrix to generate the SVG for. - :returns: the full generated SVG as a string. - """ - h = len(matrix) - w = len(matrix[0]) - - # Start SVG string - svg = f'\n' - - # Generate grid - for r in range(h): - for c in range(w): - x = c * self.square_size - y = r * self.square_size - color = matrix[r][c] - svg += f' \n' - - # Close SVG string - svg += "" - return svg - -"Main driver for the colorhash program." -import argparse -import hashlib -from pathlib import Path -import sys -import textwrap - -from .colorizer import PaletteColorizer -from .matricizer import Matricizer, NibbleMatricizer, RandomartMatricizer -from .palettes import Palette, DEFAULT_PALETTES, PALETTES -from .writer import ANSIWriter, SVGWriter - - -# TODO - WASM compile for embedding directly in HTML -# - this may not be an option, sadly. might have to just port it to JS -# TODO - option to add a caption based on the filename (for SVG) -# TODO - load palettes from a file -# TODO - PNG output - - -def cli_main() -> None: - "Main function entrypoint." - # pylint: disable=invalid-name - - MATRIX_CHOICES = { - "nibble": "Use each nibble (4 bits) of the hash to generate a matrix", - "randomart": "Use the SSH 'randomart' algorithm to generate a matrix", - } - MATRIX_HELP = "MATRIX STRATEGY (-m, --matrix)\n" + "\n".join( - f" {choice} - {desc}" for choice, desc in MATRIX_CHOICES.items() - ) - PALETTE_CHOICES = [ - "auto", - ] + list(PALETTES.keys()) - PALETTE_HELP = "\n".join( - [ - "PALETTE CHOICES", - '\n'.join(textwrap.wrap( - ", ".join(PALETTE_CHOICES), - initial_indent=" ", - subsequent_indent=" ", - )), - ] - ) - HASH_CHOICES = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"] - INPUT_TYPE_CHOICES = { - "path": "the input should be treated as a path and data is read from the path", - "hash": "the input should be treated as a hexadecimal hash (requires -a or --hash to be supplied)", - "data": "the input should be treated as raw data", - } - INPUT_TYPE_HELP = "INPUT TYPE (-x, --input-type)\n" + "\n".join( - [f" {choice} - {desc}" for choice, desc in INPUT_TYPE_CHOICES.items()] - ) - OUTPUT_TYPE_CHOICES = { - "ansi": "the output should be colored for ANSI terminals using 24 bit true color", - "svg": "the output should be an SVG format", - } - OUTPUT_TYPE_HELP = "OUTPUT TYPE (-y, --output-type)\n" + "\n".join( - [f" {choice} - {desc}" for choice, desc in OUTPUT_TYPE_CHOICES.items()] - ) - EPILOGUE = "\n\n".join([MATRIX_HELP, PALETTE_HELP, INPUT_TYPE_HELP, OUTPUT_TYPE_HELP]) - - progname: str = sys.argv[0] - if progname.endswith("__main__.py"): - progname = "colorhash" - - ap = argparse.ArgumentParser( - prog=progname, - description="Create a piece of art based on the hash of a file.", - epilog=EPILOGUE, - formatter_class=argparse.RawDescriptionHelpFormatter, - ) - - ap.add_argument( - "input", - type=str, - default="-", - help="The input to use. When acting as a path, set to '-' or blank for STDIN. Use -x or --input-type to control how input is treated. default: -", - ) - ap.add_argument( - "-o", - "--out", - metavar="OUTFILE", - type=Path, - default="-", - help="The output file to use. Set to '-' or blank for STDOUT. default: STDOUT", - ) - ap.add_argument( - "-m", - "--matrix", - metavar="MATRIX", - choices=MATRIX_CHOICES.keys(), - default="nibble", - help="Choose the strategy that turns the hash into a matrix. default: nibble", - ) - ap.add_argument( - "-p", - "--palette", - metavar="PALETTE", - choices=PALETTE_CHOICES, - default="auto", - help="Choose the palette. default: auto", - ) - ap.add_argument( - "-a", # the "a" is for "algorithm" (since -h is taken) - "--hash", - metavar="ALGORITHM", - choices=HASH_CHOICES, - # default="sha512", - required=False, - help="Choose the hash algorithm. default: sha512", - ) - ap.add_argument( - "--svg-square-size", - metavar="PX", - type=int, - default=32, - help="For SVG outputs, decide how big the output squares are, in pixels. default: 32", - ) - ap.add_argument( - "-x", - "--input-type", - default="path", - choices=INPUT_TYPE_CHOICES.keys(), - help="Determines how the input should be treated. default: path", - ) - ap.add_argument( - "-y", - "--output-type", - default="ansi", - choices=OUTPUT_TYPE_CHOICES.keys(), - help="Determines how the output should be generated. default: ansi", - ) - args = ap.parse_args() - - ############################################################################ - # End arg parsing - ############################################################################ - - # -a/--hash arg is not required when we're using file and data input types. only required for - # hash input type - if args.input_type in ("data", "path") and args.hash is None: - args.hash = "sha512" - - # Get the hash - match args.input_type: - case "path": - if args.input == "-": - infile = sys.stdin.buffer - else: - # TODO - pretty error message for when the file doesn't exist - infile = open(args.input, "rb") - # file_digest (I hope) will not load too much into memory - hashdata = hashlib.file_digest(infile, args.hash).digest() # type: ignore - # NOTE : previous line has typing ignored because file_digest requires a - # "_BytesIOLike | _FileDigestFileObj", both of which look like API leaks. Specifying - # infile to be BinaryIO is not enough and causes the same error. - case "hash": - # TODO - maybe a better error message? - if args.hash is None: - print( - "ERROR: -a or --hash should be supplied on the command line when using the hash input type", - file=sys.stderr, - ) - raise SystemExit(1) - # TODO - pretty error message for malformed input - hashdata = bytes([int(byte, 16) for byte in textwrap.wrap(args.input, 2)]) - case "data": - hashdata = hashlib.new(args.hash, args.input.encode()).digest() - case _: - assert False, f"unknown input type {args.input_type}" - - # Choose the dimensions and the matricizer - matricizer: Matricizer - match args.matrix: - case "nibble": - w, h = NibbleMatricizer.DIMENSIONS[args.hash] - matricizer = NibbleMatricizer(w, h) - case "randomart": - # 17x9 is what openssh uses - # TODO - allow configuring dimensions, maybe - w, h = RandomartMatricizer.DIMENSIONS[args.hash] - matricizer = RandomartMatricizer(w, h) - case _: - assert False, f"invalid args.matrix: {args.matrix}" - - # Choose the palette - palette: Palette - if args.palette == "auto": - palette = matricizer.choose_palette(hashdata) - else: - palette = PALETTES[args.palette] - - # Choose the colorizer - colorizer = PaletteColorizer(palette) - - # Print SVG - matrix = matricizer.matricize(hashdata) - colors = colorizer.colorize(matrix) - - # Choose the output writer - writer: Writer - match args.output_type: - case "ansi": - writer = ANSIWriter() - case "svg": - writer = SVGWriter(args.svg_square_size) - - output = writer.write(colors) - - if str(args.out) == "-": - sys.stdout.write(output) - else: - args.out.write_text(output) - diff --git a/tools/genexamples.sh b/tools/genexamples.sh index ed614cf..3934367 100755 --- a/tools/genexamples.sh +++ b/tools/genexamples.sh @@ -7,9 +7,6 @@ matrices=(nibble randomart) cd "$here/.." -# Additionally, get the full source code output as an "in" file, and use that as an example too. -find "colorhash" -type f -name '*.py' -exec cat '{}' ';' >examples/fullsource.in - find "examples" -type f -name '*.in' | \ while read infile; do for hash in "${hashes[@]}"; do @@ -23,3 +20,7 @@ while read infile; do done done done + +# Additionally, create an SVG for the current commit hash +echo "Generating examples/commithash.svg" +python3 -m colorhash "$(git rev-parse HEAD)" --input-type hash --hash sha1 --matrix randomart --output-type svg --out examples/commithash.svg