sciagram 0.0.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,47 @@
1
+ Metadata-Version: 2.3
2
+ Name: sciagram
3
+ Version: 0.0.1
4
+ Summary: easy and fun ASCII art generation!
5
+ Author: Ishuman
6
+ Author-email: Ishuman <ishuman2006@gmail.com>
7
+ Requires-Dist: pillow>=12.3.0
8
+ Requires-Python: >=3.13
9
+ Description-Content-Type: text/markdown
10
+
11
+ <p align="center">
12
+ <img src="./assets/demo_bw.png" alt="demo ASCII art generated using sciagram" />
13
+ </p>
14
+ <h1 align="center">sciagram!</h1>
15
+ <p align="center">Fun little ASCII art generation project</p>
16
+
17
+ ## Introduction
18
+ With _sciagram_ I aim to create my personal ASCII art generation toolkit.
19
+
20
+ I have always loved ASCII art, it just looks beautiful and gives me an odd non-existing nostalgia about the way people expressed art on the internet before I was born. Also, for as long as I can remember, I have wanted to implement an ASCII art generator; and today I was bored, so here goes nothing! I followed [this guide](https://robertheaton.com/2018/06/12/programming-projects-for-advanced-beginners-ascii-art/) and I can't say it was not fun to program.
21
+
22
+ _sciagram_ has a single dependency - [Pillow](https://pypi.org/project/pillow/), for image modification.
23
+
24
+ ## Usage
25
+ This is aimed at a PyPI release, but I haven't really done that yet as the code is still a bit "weak" and not package-worthy. So in caveman-fashion I humbly ask you to generate images manually for now :)
26
+ 1. Clone the repository locally
27
+ ```bash
28
+ git clone https://github.com/cosmognaut/sciagram.git
29
+ cd sciagram
30
+ ```
31
+ 2. Move an image of yours to the root folder if you want to run the program from there. You can also do this inside `src/sciagram/`.
32
+ ```bash
33
+ mv sample.jpg path-to-clone/
34
+ ```
35
+ 3. You can now just use `uv run` to run the program, it will automatically install dependencies for you and activate the virtual environment!
36
+ ```bash
37
+ uv run -m src.sciagram.main
38
+ ```
39
+ This "caveman" method will be changed in the future, for sure.
40
+
41
+ ## Ideas
42
+ Right now the code is in a very rudimentary stage, so expect a LOT of changes. Some ideas I have been interested in:
43
+ 1. Restructuring the code so that I can actually use this as a module, this should be easy - just OOP this shit into oblivion.
44
+ 2. `sciagram` CLI tool for easy generation on the fly.
45
+ 3. RGB color generation mode. Should be easy with Pillow. I already have the RGB tuples and I just need to wrap each printed character with ANSI escape codes. (PRIORITY)
46
+ 4. Local brightness exploration, ex. if a terminal cell is 20px by 10px then I could divide the image into blocks of 20x10 pixels and calculate a "local" brightness for that, and use unicode characters this time for brightness. Still B&W but maybe cooler?
47
+ 5. A mechanism for preserving original image proportions during manipulation. Right now we are following a naive rule where we just scale up/down to `(term_cols, term_rows)`. (PRIORITY)
@@ -0,0 +1,37 @@
1
+ <p align="center">
2
+ <img src="./assets/demo_bw.png" alt="demo ASCII art generated using sciagram" />
3
+ </p>
4
+ <h1 align="center">sciagram!</h1>
5
+ <p align="center">Fun little ASCII art generation project</p>
6
+
7
+ ## Introduction
8
+ With _sciagram_ I aim to create my personal ASCII art generation toolkit.
9
+
10
+ I have always loved ASCII art, it just looks beautiful and gives me an odd non-existing nostalgia about the way people expressed art on the internet before I was born. Also, for as long as I can remember, I have wanted to implement an ASCII art generator; and today I was bored, so here goes nothing! I followed [this guide](https://robertheaton.com/2018/06/12/programming-projects-for-advanced-beginners-ascii-art/) and I can't say it was not fun to program.
11
+
12
+ _sciagram_ has a single dependency - [Pillow](https://pypi.org/project/pillow/), for image modification.
13
+
14
+ ## Usage
15
+ This is aimed at a PyPI release, but I haven't really done that yet as the code is still a bit "weak" and not package-worthy. So in caveman-fashion I humbly ask you to generate images manually for now :)
16
+ 1. Clone the repository locally
17
+ ```bash
18
+ git clone https://github.com/cosmognaut/sciagram.git
19
+ cd sciagram
20
+ ```
21
+ 2. Move an image of yours to the root folder if you want to run the program from there. You can also do this inside `src/sciagram/`.
22
+ ```bash
23
+ mv sample.jpg path-to-clone/
24
+ ```
25
+ 3. You can now just use `uv run` to run the program, it will automatically install dependencies for you and activate the virtual environment!
26
+ ```bash
27
+ uv run -m src.sciagram.main
28
+ ```
29
+ This "caveman" method will be changed in the future, for sure.
30
+
31
+ ## Ideas
32
+ Right now the code is in a very rudimentary stage, so expect a LOT of changes. Some ideas I have been interested in:
33
+ 1. Restructuring the code so that I can actually use this as a module, this should be easy - just OOP this shit into oblivion.
34
+ 2. `sciagram` CLI tool for easy generation on the fly.
35
+ 3. RGB color generation mode. Should be easy with Pillow. I already have the RGB tuples and I just need to wrap each printed character with ANSI escape codes. (PRIORITY)
36
+ 4. Local brightness exploration, ex. if a terminal cell is 20px by 10px then I could divide the image into blocks of 20x10 pixels and calculate a "local" brightness for that, and use unicode characters this time for brightness. Still B&W but maybe cooler?
37
+ 5. A mechanism for preserving original image proportions during manipulation. Right now we are following a naive rule where we just scale up/down to `(term_cols, term_rows)`. (PRIORITY)
@@ -0,0 +1,16 @@
1
+ [project]
2
+ name = "sciagram"
3
+ version = "0.0.1"
4
+ description = "easy and fun ASCII art generation!"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Ishuman", email = "ishuman2006@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.13"
10
+ dependencies = [
11
+ "pillow>=12.3.0",
12
+ ]
13
+
14
+ [build-system]
15
+ requires = ["uv_build>=0.11.17,<0.12.0"]
16
+ build-backend = "uv_build"
@@ -0,0 +1,2 @@
1
+ def hello() -> str:
2
+ return "Hello from sciagram!"
Binary file
@@ -0,0 +1,148 @@
1
+ import os
2
+ from PIL import Image
3
+ from typing import Literal
4
+
5
+ type PixelMatrix = list[list[tuple[int, int, int]]]
6
+ type GenericMatrix = list[list[float]]
7
+
8
+ def _calculate_brightness(red: int, green: int, blue: int, method: Literal["average", "min_max", "luminosity"] = "average") -> float:
9
+ """Calculate the brightness for given RGB values"""
10
+ if method == "average":
11
+ return (red + green + blue) / 3
12
+ elif method == "min_max":
13
+ return ((max(red, green, blue) + min(red, green, blue)) / 2)
14
+ elif method == "luminosity":
15
+ return (0.21 * red + 0.72 * green + 0.07 * blue)
16
+
17
+ def _brightness_to_ascii(brightness: float, sequence: str = "`^\",:;Il!i~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$") -> str:
18
+ """Returns an ASCII character for any brightness value"""
19
+ # ASCII_CHARS = "`^\",:;Il!i~+_-?][}{1)(|\\/tfjrxnuvczXYUJCLQ0OZmwqpdbkhao*#MW&8%B@$"
20
+ # think: 0 brightness gets index 0, max brightness gets index 64.
21
+ # which means 0 maps to 0, 255 maps to 64.
22
+ # for max 100 brightness and 20 chars, I can do 50 / 100 * 20
23
+ # following the above logic, 49 should map to 10 again. It gets 9.8. And 10.4 which
24
+ # is 52 should also get the same thing? The range here is 9 = x . 0.2 and 11 = y . 0.2 viz 45 - 55. (0.2 is just 20/100)
25
+ # for max 255 brightness and 64 chars can I do x / 255 * 64?
26
+ # and for this case, what's the range here, i.e. in what range do you get the same brightness? In the above example it was 10, for ex. in strip 45-55 you would find the same brightness.
27
+ # lower = x . 1/255*64 and higher = y . 1/255*64. So it's 0.25098039215686274 times whatever. Here the midpoint would be 64/2 = 32.
28
+ # But wait - I will never see the $ sign this way? As let's say the brightness for a pixel here would be 252 and I get 63.2 which I can round to 63 to get @ sign.
29
+ # What if I take brightness of a pixel to be 254? I get 63.7 then which I can round to 64. So this does seem to work?
30
+ # Let me try to apply this.
31
+ brightness_number = round((brightness/255) * (len(sequence) - 1))
32
+ return sequence[brightness_number]
33
+
34
+ def load_image(image_path: str, true_term: bool = True) -> Image.Image:
35
+ """Load an image and optionally resize it to then default terminal cell dimensions (1 char per cell corresponding to 1px)"""
36
+ image = Image.open(image_path)
37
+ size = os.get_terminal_size()
38
+ if true_term:
39
+ term_cols, term_rows = size.columns, size.lines
40
+ # effective_height = round(term_rows * 0.5) # aspect ratio correction is taken to be 0.5
41
+ image = image.resize((term_cols, term_rows))
42
+ print("Successfully loaded image!")
43
+ print(f"Image size: {image.size}")
44
+ return image
45
+
46
+ def generate_pixel_matrix(image: Image.Image) -> PixelMatrix:
47
+ """Generate a pixel matrix for a given image, with a given width and height"""
48
+ # pixel_matrix = [pixel for pixel in sample.get_flattened_data()]
49
+ # get_flattened_data gives, well, flattened data, which I don't want for my 2D array.
50
+
51
+ # coordinates are represented as 0,0 on the upper left
52
+ # width is 700, height is 486 or whatever.
53
+ # so this matrix should be of size 467, each array containing 700 pixel values.
54
+ flat_data = image.get_flattened_data()
55
+ pixel_matrix = []
56
+ image_height = image.height
57
+ image_width = image.width
58
+ # I spent like 30 mins discovering this formula
59
+ # but I had also discovered that flat_data was managing tuples in terms of y times width
60
+ # it genuinely didn't cross my mind to just append that instead of going for the pixel-perfect tuple (literally)
61
+ # for y in range(image_height):
62
+ # pixel_row = []
63
+ # for x in range(image_width):
64
+ # pixel_at_coordinate = flat_data[(y * image_width) + x]
65
+ # pixel_row.append(pixel_at_coordinate)
66
+ # pixel_matrix.append(pixel_row)
67
+ for y in range(image_height):
68
+ pixel_matrix.append(flat_data[(y * image_width) : ((y+1) * image_width)])
69
+ return pixel_matrix
70
+
71
+ def generate_brightness_matrix(pixel_matrix: PixelMatrix, method: Literal["average", "min_max", "luminosity"] = "average") -> GenericMatrix:
72
+ """Give each pixel a brightness value and generate a new matrix based on that"""
73
+ brightness_matrix = []
74
+ for pixel_row in pixel_matrix:
75
+ brightness_row = []
76
+ for pixel in pixel_row:
77
+ red = pixel[0]
78
+ green = pixel[1]
79
+ blue = pixel[2]
80
+ pixel_brightness = _calculate_brightness(red, green, blue, method=method)
81
+ brightness_row.append(pixel_brightness)
82
+ brightness_matrix.append(brightness_row)
83
+ return brightness_matrix
84
+
85
+ # now I need to scale each brightness to an ASCII character.
86
+ # len(ASCII_CHARS) is 65. A brightness of 0 should pick the first character, that is the backtick ` at 0 and a brightness of 255 should pick the last character, i.e. the 64th and final character $.
87
+
88
+ def generate_ascii_matrix(brightness_matrix: GenericMatrix) -> GenericMatrix:
89
+ """Generate the final ASCII matrix containing an ASCII character for each pixel/brightness value"""
90
+ ascii_matrix = []
91
+ for row in brightness_matrix:
92
+ ascii_row = []
93
+ for brightness_pixel in row:
94
+ ascii_row.append(_brightness_to_ascii(brightness_pixel))
95
+ ascii_matrix.append(ascii_row)
96
+ return ascii_matrix
97
+
98
+ # 700 width 467 height or 700 height 467 width?
99
+
100
+ # four_six_seven_chars = []
101
+ # for _ in range(467):
102
+ # four_six_seven_chars.append("a")
103
+ #
104
+ # for char in four_six_seven_chars:
105
+ # print(char, end="")
106
+
107
+ # for my terminal stty size gives 35 173 at default settings. Which means 35 cells of height and 173 cells of width. One cell is configured to be 14pt in ghostty's font settings. 14pt is around 18.66 pixels.
108
+ # Fuck, let me do some trial and error and see on which dimensions my image is rendering perfectly on my screen. Let me look up resizing on pillow.
109
+ # on stty size 1036 948 the image renders perfectly.
110
+ # this is tough - if I resize an image before converting it to raw pixel data I would lose quality, by a lot.
111
+
112
+ # 0,1 is on 40, (0, 2) is on 80 and so on. 40 is the image width.
113
+ # (1,0) is on 1, (2, 0) is on 2. (2, 1) is on 42. (4,1) is on 44. (4,3) is on 124. (5,3) is on 125.
114
+ # (11, 3) is on 131. So again, 3 * 40 + 11 = 131.
115
+ # the formula seems to be flat_data[(width * y coordinate) + x coordinate]
116
+
117
+ def optimised_generation(image: Image.Image, method: Literal["average", "min_max", "luminosity"] = "average") -> GenericMatrix:
118
+ """
119
+ Optimised generation of the final ASCII characters' matrix.
120
+ Uses a single iteration to accomplish everything.
121
+ """
122
+ flat_data = image.get_flattened_data()
123
+ final_matrix = []
124
+ image_height = image.height
125
+ image_width = image.width
126
+ for y in range(image_height):
127
+ character_row = []
128
+ for x in range(image_width):
129
+ pixel = flat_data[(y * image_width) + x]
130
+ assert type(pixel) is tuple # for linter errors
131
+ red = pixel[0]
132
+ green = pixel[1]
133
+ blue = pixel[2]
134
+ pixel_brightness = _calculate_brightness(red, green, blue, method=method)
135
+ character_row.append(_brightness_to_ascii(pixel_brightness))
136
+ final_matrix.append(character_row)
137
+ return final_matrix
138
+
139
+ if __name__ == "__main__":
140
+ image = load_image("cosmog.jpg", true_term=True)
141
+ final_mat = optimised_generation(image, method="luminosity")
142
+ # pixel_mat = generate_pixel_matrix(image)
143
+ # brightness_mat = generate_brightness_matrix(pixel_mat)
144
+ # final_mat = generate_ascii_matrix(brightness_mat)
145
+ for row in final_mat:
146
+ for char in row:
147
+ print(char, end="")
148
+ print()
File without changes
Binary file
@@ -0,0 +1,9 @@
1
+ import os
2
+
3
+ size = os.get_terminal_size()
4
+ print(size)
5
+ print(size.columns, size.lines)
6
+
7
+
8
+ for _ in range(10):
9
+ print('#' * 10)