quantum-pixel 2.0.4__tar.gz → 2.2.0__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.
Files changed (42) hide show
  1. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/Cargo.lock +1 -1
  2. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/Cargo.toml +1 -3
  3. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/PKG-INFO +5 -4
  4. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/README.md +1 -1
  5. {quantum_pixel-2.0.4/quantum_pixel → quantum_pixel-2.2.0}/README_PYPI.md +1 -1
  6. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/pyproject.toml +9 -3
  7. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0/quantum_pixel}/README_PYPI.md +1 -1
  8. quantum_pixel-2.2.0/quantum_pixel/generator.py +74 -0
  9. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/main.py +4 -1
  10. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/decode.js +9 -1
  11. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/encode.js +35 -6
  12. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/styles.css +2 -2
  13. quantum_pixel-2.2.0/quantum_pixel/static/upload.js +36 -0
  14. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/decode.html +2 -0
  15. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/encode.html +37 -4
  16. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/index.html +6 -5
  17. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/web.py +74 -54
  18. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/uv.lock +15 -15
  19. quantum_pixel-2.0.4/quantum_pixel/generator.py +0 -79
  20. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.github/FUNDING.yml +0 -0
  21. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.github/workflows/CI.yml +0 -0
  22. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.github/workflows/render-md-mermaid.yml +0 -0
  23. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.gitignore +0 -0
  24. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.python-version +0 -0
  25. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/LICENSE +0 -0
  26. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/encoded.png +0 -0
  27. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/grok.png +0 -0
  28. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/intro.png +0 -0
  29. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/material.png +0 -0
  30. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/mermaid.png +0 -0
  31. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/preview.png +0 -0
  32. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/__init__.py +0 -0
  33. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/favicon.svg +0 -0
  34. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/robots.txt +0 -0
  35. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/steganography.py +0 -0
  36. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/error.html +0 -0
  37. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/result.html +0 -0
  38. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/render.yaml +0 -0
  39. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/requirements.txt +0 -0
  40. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/src/lib.rs +0 -0
  41. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/tests/test_generator.py +0 -0
  42. {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/tests/test_steganography.py +0 -0
@@ -773,7 +773,7 @@ dependencies = [
773
773
 
774
774
  [[package]]
775
775
  name = "quantum-pixel"
776
- version = "2.0.4"
776
+ version = "2.2.0"
777
777
  dependencies = [
778
778
  "pyo3",
779
779
  "stegano-core",
@@ -1,9 +1,7 @@
1
1
  [package]
2
2
  edition = "2024"
3
3
  name = "quantum-pixel"
4
- version = "2.0.4"
5
- authors = ["Linos1391 <linos.coding@gmail.com>"]
6
- homepage = "https://github.com/Linos1391/quantum-pixel.git"
4
+ version = "2.2.0"
7
5
  description = "When both YES and NO are existed. Only human with emotions can comprehend."
8
6
  readme = "README_PYPI.md"
9
7
  license = "GNU General Public License v3 (GPLv3)"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: quantum-pixel
3
- Version: 2.0.4
3
+ Version: 2.2.0
4
4
  Classifier: Development Status :: 5 - Production/Stable
5
5
  Classifier: Natural Language :: English
6
6
  Classifier: Programming Language :: Rust
@@ -14,11 +14,12 @@ Requires-Dist: numpy>=2.4.0
14
14
  Requires-Dist: pillow>=12.0.0
15
15
  License-File: LICENSE
16
16
  Summary: When both YES and NO are existed. Only human with emotions can comprehend.
17
- Home-Page: https://github.com/Linos1391/quantum-pixel.git
18
- Author: Linos1391 <linos.coding@gmail.com>
19
17
  Author-email: Linos1391 <linos.coding@gmail.com>
20
18
  Requires-Python: >=3.14
21
19
  Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
20
+ Project-URL: funding, https://github.com/sponsors/Linos1391
21
+ Project-URL: issues, https://github.com/Linos1391/quantum-pixel/issues
22
+ Project-URL: repository, https://github.com/Linos1391/quantum-pixel.git
22
23
 
23
24
  <div align="center">
24
25
 
@@ -109,7 +110,7 @@ Really slow and often exceed RAM limit.
109
110
 
110
111
  - For private use or sharing with friends? Absolutely yes. I am using the free version of Render right now and totally recommend to try. Import `render.yml` as blueprint.
111
112
 
112
- - For your website? You may embed this project as it is and let it be entirely free.
113
+ - For your website? You may embed this project and let it be entirely free. Optimize my garbage how ever you desire, though.
113
114
 
114
115
  <br>
115
116
 
@@ -87,7 +87,7 @@ Really slow and often exceed RAM limit.
87
87
 
88
88
  - For private use or sharing with friends? Absolutely yes. I am using the free version of Render right now and totally recommend to try. Import `render.yml` as blueprint.
89
89
 
90
- - For your website? You may embed this project as it is and let it be entirely free.
90
+ - For your website? You may embed this project and let it be entirely free. Optimize my garbage how ever you desire, though.
91
91
 
92
92
  <br>
93
93
 
@@ -87,7 +87,7 @@ Really slow and often exceed RAM limit.
87
87
 
88
88
  - For private use or sharing with friends? Absolutely yes. I am using the free version of Render right now and totally recommend to try. Import `render.yml` as blueprint.
89
89
 
90
- - For your website? You may embed this project as it is and let it be entirely free.
90
+ - For your website? You may embed this project and let it be entirely free. Optimize my garbage how ever you desire, though.
91
91
 
92
92
  <br>
93
93
 
@@ -4,12 +4,13 @@ build-backend = "maturin"
4
4
 
5
5
  [project]
6
6
  name = "quantum-pixel"
7
+ authors = [
8
+ { name = "Linos1391", email = "linos.coding@gmail.com" }
9
+ ]
7
10
  dynamic = [
8
11
  "version",
9
- "authors",
10
12
  "description",
11
- "readme",
12
- "repository"
13
+ "readme"
13
14
  ]
14
15
  requires-python = ">=3.14"
15
16
  dependencies = [
@@ -28,6 +29,11 @@ classifiers=[
28
29
  "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
29
30
  ]
30
31
 
32
+ [project.urls]
33
+ repository = "https://github.com/Linos1391/quantum-pixel.git"
34
+ issues = "https://github.com/Linos1391/quantum-pixel/issues"
35
+ funding = "https://github.com/sponsors/Linos1391"
36
+
31
37
  [project.scripts]
32
38
  quantum_pixel = "quantum_pixel.main:main"
33
39
 
@@ -87,7 +87,7 @@ Really slow and often exceed RAM limit.
87
87
 
88
88
  - For private use or sharing with friends? Absolutely yes. I am using the free version of Render right now and totally recommend to try. Import `render.yml` as blueprint.
89
89
 
90
- - For your website? You may embed this project as it is and let it be entirely free.
90
+ - For your website? You may embed this project and let it be entirely free. Optimize my garbage how ever you desire, though.
91
91
 
92
92
  <br>
93
93
 
@@ -0,0 +1,74 @@
1
+ # cspell:ignore setdiff, fromarray
2
+ """
3
+ Generating layers from an image file.
4
+ """
5
+
6
+ import logging
7
+ import os
8
+ from random import randint
9
+
10
+ import numpy as np
11
+ from PIL import Image
12
+
13
+ class Generator:
14
+ """
15
+ The generator.
16
+ """
17
+ def __init__(self, input_path: str):
18
+ try: # convert to RGB to remove the only way AI can learn to solve.
19
+ self.img_data = np.array(Image.open(input_path).convert("RGB"))
20
+ except Exception as e:
21
+ logging.error("Error opening image: %s", e)
22
+ raise e
23
+ self._allowance: int = -1
24
+ self._remain_allowance: int = -1
25
+
26
+ def get_progress(self):
27
+ """You get the idea, and the course to the destination. (type shi)"""
28
+ return int(100*(1-self._remain_allowance/self._allowance))
29
+
30
+
31
+ def preview(self, intensity: float, output_path: str) -> None:
32
+ """
33
+ Generate the preview layer AND STREAM PROGRESS OUT.
34
+ ```
35
+ generator = Generator("Path/to/image.png")
36
+ generator.preview(0.5, "Path/to/output/image.png")
37
+ ```
38
+
39
+ Args:
40
+ intensity (float): the amount of pixel being taken (0-1). The smaller it is, the \
41
+ faster to process, the harder to visualize. (AI may have the stroke, and so \
42
+ human's eyes)
43
+ output_path (str): Where the file should be saved at.
44
+ """
45
+ assert 0 <= intensity <= 1, "Invalid intensity"
46
+
47
+ # [[i, j] for i in range(img_data.shape[0]) for j in range(img_data.shape[1])]
48
+ available_location = np.stack(np.meshgrid(np.arange(self.img_data.shape[0]),
49
+ np.arange(self.img_data.shape[1])), axis=-1).reshape(-1, 2)
50
+ np.random.shuffle(available_location)
51
+
52
+ # do the shit.
53
+ self._allowance = int(int(np.sum(self.img_data) * intensity))
54
+ self._remain_allowance = self._allowance
55
+
56
+ layer: np.ndarray = np.zeros_like(self.img_data)
57
+ index = 0
58
+ while self._remain_allowance > 0:
59
+ try:
60
+ location = tuple(available_location[index].tolist())
61
+ index += 1
62
+ except IndexError:
63
+ break
64
+ for current_value in range(3): # RGBA channels
65
+ value=min (randint(0, self.img_data[location][current_value]),
66
+ self._remain_allowance)
67
+ layer[location][current_value] = value
68
+ self._remain_allowance -= value
69
+ os.makedirs(os.path.dirname(output_path), exist_ok=True)
70
+ Image.fromarray(layer.astype(np.uint8), "RGB").save(output_path)
71
+
72
+ if __name__ == "__main__":
73
+ generator = Generator("assets/material.png")
74
+ generator.preview(0.5, "output/output.png")
@@ -3,8 +3,11 @@
3
3
  import os
4
4
 
5
5
  import uvicorn
6
- from . import FastAPIApp
6
+ from quantum_pixel import FastAPIApp
7
7
 
8
8
  def main():
9
9
  """Run the web with script"""
10
10
  uvicorn.run(FastAPIApp, host="0.0.0.0", port=int(os.environ.get("PORT", 8000)))
11
+
12
+ if __name__ == "__main__":
13
+ main()
@@ -16,8 +16,8 @@
16
16
  });
17
17
  const json = await r.json();
18
18
 
19
+ const error = document.getElementById("error");
19
20
  if (json.error) {
20
- const error = document.getElementById("error");
21
21
  error.style.display = "";
22
22
  error.innerHTML = json.error;
23
23
  } else {
@@ -25,8 +25,12 @@
25
25
  }
26
26
 
27
27
  if (json.result) {
28
+ result.style.display = "";
28
29
  result.innerHTML = json.result;
30
+ } else {
31
+ result.style.display = "none";
29
32
  }
33
+
30
34
  } catch (err) {
31
35
  console.error(err);
32
36
  }
@@ -34,6 +38,10 @@
34
38
 
35
39
  // remove file when user left.
36
40
  window.addEventListener('beforeunload', () => {
41
+ const error = document.getElementById("error");
42
+ error.style.display = "";
43
+ error.innerHTML = "ExitError: You just exit and got auto cleanup!";
44
+
37
45
  fetch(`/remove/${location.pathname.split("/").pop()}`, {
38
46
  method: 'POST',
39
47
  keepalive: true
@@ -22,6 +22,13 @@
22
22
  form.addEventListener('submit', async (e) => {
23
23
  e.preventDefault();
24
24
  const fd = new FormData(form);
25
+
26
+ if ((fd.get("image") === "custom" && fd.get("image_file").size === 0) ||
27
+ (fd.get("disguise") === "custom" && fd.get("disguise_file").size === 0)) {
28
+ alert("Please select custom file!");
29
+ return false;
30
+ }
31
+
25
32
  const selected = document.querySelector('#tabs-headers .tab-btn.active').dataset.target;
26
33
  fd.append('selected', selected);
27
34
 
@@ -29,18 +36,22 @@
29
36
  result.style.display = "";
30
37
  result.textContent = 'Loading, please wait patiently.';
31
38
 
32
- const TIMEOUT = 300_000; // 5 minutes - NodeJS default anyway.
33
39
  try {
40
+ const TIMEOUT = 300_000; // 5 minutes.
41
+ const controller = new AbortController();
42
+ const timeoutId = setTimeout(() => controller.abort(), TIMEOUT);
43
+
34
44
  const r = await fetch(location.pathname, {
35
45
  method: 'POST',
36
46
  body: fd,
37
- signal: AbortSignal.timeout(TIMEOUT)
47
+ signal: controller.signal
38
48
  });
39
49
 
40
50
  const reader = r.body.getReader();
41
51
  const decoder = new TextDecoder();
42
52
  let buffer = "";
43
53
 
54
+ let end_timeout = true
44
55
  while (true) {
45
56
  const { value, done } = await reader.read();
46
57
  if (done) {
@@ -49,6 +60,7 @@
49
60
 
50
61
  const error = document.getElementById("error");
51
62
  if (json.error) {
63
+
52
64
  error.style.display = "";
53
65
  error.innerHTML = json.error;
54
66
  } else {
@@ -56,10 +68,27 @@
56
68
  }
57
69
 
58
70
  if (json.result) {
71
+ result.style.display = "";
59
72
  result.innerHTML = json.result;
73
+
74
+ if (selected === "panel_preview") {
75
+ divs = document.getElementsByClassName("preview-input");
76
+ Array.from(divs).forEach(function(div){
77
+ div.style.display = "";
78
+ })
79
+ } else if (selected === "panel_resize") {
80
+ div = document.getElementsByClassName("resize-input")[0];
81
+ div.style.display = "";
82
+ }
83
+ } else {
84
+ result.style.display = "none";
60
85
  }
61
86
  }
62
87
  break;
88
+ }
89
+ else if (end_timeout) {
90
+ end_timeout = false;
91
+ clearTimeout(timeoutId);
63
92
  };
64
93
 
65
94
  buffer += decoder.decode(value, { stream: true });
@@ -90,13 +119,13 @@
90
119
 
91
120
  // remove file when user left.
92
121
  window.addEventListener('beforeunload', () => {
122
+ const error = document.getElementById("error");
123
+ error.style.display = "";
124
+ error.innerHTML = "ExitError: You just exit and got auto cleanup!";
125
+
93
126
  fetch(`/remove/${location.pathname.split("/").pop()}`, {
94
127
  method: 'POST',
95
128
  keepalive: true
96
129
  });
97
130
  });
98
131
  })();
99
-
100
-
101
- // TODO
102
- // - error did not display initially! --> error.html wraps everything.
@@ -85,8 +85,9 @@ button, .primary {
85
85
 
86
86
  .panel-controls { margin-top:0.6rem; display:flex; flex-direction:column; gap:0.5rem; flex:1; }
87
87
  .panel-controls div { align-items:center; display:flex; flex-direction:row; gap:0.5rem; width: 100%; }
88
- .panel-controls label { font-size:0.9rem; color:var(--text); }
88
+ .panel-controls label { padding:0.35rem; font-size:0.9rem; color:var(--text); }
89
89
  .panel-controls input { padding:0.35rem; border:1px solid rgba(255, 255, 255, 0.25); border-radius:6px; background:transparent; color:var(--text); flex: 1 0 auto }
90
+ .panel-controls input[type=radio] { flex-grow: 0; }
90
91
  .panel-controls button { padding: 0.6rem 0.9rem; display:block; width:100%; }
91
92
 
92
93
  .tabs { margin-top:0.8rem; display:flex; flex-direction:column; flex:1; width:100%; }
@@ -102,5 +103,4 @@ button, .primary {
102
103
  .container { width: 100%; padding: 1rem; }
103
104
  form { flex-direction: column; align-items: stretch; }
104
105
  input[type=file] { width: 100%; }
105
- button { width: 100%; }
106
106
  }
@@ -0,0 +1,36 @@
1
+ (async function () {
2
+ const form = document.getElementsByClassName('upload-form')[0];
3
+ form.addEventListener('submit', async (e) => {
4
+ e.preventDefault();
5
+
6
+ const result = document.getElementById("result");
7
+ result.style.display = "";
8
+ result.innerHTML = "Loading, please wait patiently. (If the wait time is too long, it is recommended to use the local version.)";
9
+
10
+ const fd = new FormData(form, e.submitter);
11
+
12
+ try {
13
+ const r = await fetch(location.pathname, {
14
+ method: 'POST',
15
+ body: fd
16
+ });
17
+ const json = await r.json();
18
+
19
+ const error = document.getElementById("error");
20
+ if (json.error) {
21
+ result.style.display = "none";
22
+ error.style.display = "";
23
+ error.innerHTML = json.error;
24
+ } else {
25
+ error.style.display = "none";
26
+ }
27
+
28
+ if (json.redirect) {
29
+ window.location.href = json.redirect;
30
+ }
31
+
32
+ } catch (err) {
33
+ console.error(err);
34
+ }
35
+ });
36
+ })();
@@ -15,6 +15,8 @@
15
15
  <form class="panel-controls" enctype="multipart/form-data">
16
16
  <div>
17
17
  <label>Password (Leave blank if don't use)</label>
18
+ </div>
19
+ <div>
18
20
  <input name="password" type="password"/>
19
21
  </div>
20
22
  <button id="apply-btn" class="actions">Submit</button>
@@ -54,15 +54,27 @@
54
54
  <form class="panel-controls" enctype="multipart/form-data">
55
55
  <div>
56
56
  <label>Chosen Image</label>
57
- <input name="image" type="file" accept="image/*" required/>
58
57
  </div>
58
+ <div>
59
+ <input name="image" type="radio" value="uploaded" checked/>
60
+ <label><em>Uploaded Image</em></label>
61
+ </div>
62
+ <div class="preview-input" style="display: none;">
63
+ <input name="image" type="radio" value="preview"/>
64
+ <label><em>Preview Image</em></label>
65
+ </div>
66
+ <div>
67
+ <input name="image" type="radio" value="custom"/>
68
+ <input name="image_file" type="file" accept="image/*"/>
69
+ </div>
70
+
59
71
  <div>
60
72
  <label>Width</label>
61
- <input name="width" type="number" min="0" placeholder="(width pixel)" required/>
73
+ <input name="width" type="number" min="1" placeholder="(width pixel)" required/>
62
74
  </div>
63
75
  <div>
64
76
  <label>Length</label>
65
- <input name="length" type="number" min="0" placeholder="(length pixel)" required/>
77
+ <input name="length" type="number" min="1" placeholder="(length pixel)" required/>
66
78
  </div>
67
79
  <button id="apply-btn" class="actions primary">Apply</button>
68
80
  </form>
@@ -74,10 +86,31 @@
74
86
  <form class="panel-controls" enctype="multipart/form-data">
75
87
  <div>
76
88
  <label>Disguise Image</label>
77
- <input name="disguise" type="file" accept="image/*" required/>
78
89
  </div>
90
+ <div>
91
+ <input name="disguise" type="radio" value="uploaded" checked/>
92
+ <label><em>Uploaded Image</em></label>
93
+ </div>
94
+
95
+ <div class="preview-input" style="display: none;">
96
+ <input name="disguise" type="radio" value="preview"/>
97
+ <label><em>Preview Image</em></label>
98
+ </div>
99
+
100
+ <div class="resize-input" style="display: none;">
101
+ <input name="disguise" type="radio" value="resize"/>
102
+ <label><em>Resize Image</em></label>
103
+ </div>
104
+
105
+ <div>
106
+ <input name="disguise" type="radio" value="custom"/>
107
+ <input name="disguise_file" type="file" accept="image/*"/>
108
+ </div>
109
+
79
110
  <div>
80
111
  <label>Password (not required but recommend)</label>
112
+ </div>
113
+ <div>
81
114
  <input name="password" type="password"/>
82
115
  </div>
83
116
  <button id="apply-btn" class="actions primary">Apply</button>
@@ -11,15 +11,16 @@
11
11
  <main class="container">
12
12
  <h1>Upload an Image (Only accept PNG)</h1>
13
13
 
14
- {% if error %}
15
- <div class="error">{{ error }}</div>
16
- {% endif %}
14
+ <div id="error" style="display: none;"></div>
17
15
 
18
- <form class="upload-form" method="post" enctype="multipart/form-data">
19
- <input type="file" name="file" accept="image/png" required>
16
+ <form class="upload-form" enctype="multipart/form-data">
17
+ <input type="file" name="file" accept="image/*" required>
20
18
  <button type="submit" name="upload_type" value="encode">Encode</button>
21
19
  <button type="submit" name="upload_type" value="decode">Decode</button>
22
20
  </form>
21
+
22
+ <div id="result" style="display: none;"></div>
23
23
  </main>
24
24
  </body>
25
+ <script src="/static/upload.js"></script>
25
26
  </html>
@@ -6,13 +6,12 @@ import os
6
6
  import time
7
7
  import asyncio
8
8
  import json
9
- import types
10
9
  from concurrent.futures.thread import ThreadPoolExecutor
11
10
  from contextlib import asynccontextmanager
12
11
 
13
12
  from fastapi import FastAPI, Request
14
13
  from fastapi.middleware.cors import CORSMiddleware
15
- from fastapi.responses import HTMLResponse, RedirectResponse, JSONResponse, StreamingResponse
14
+ from fastapi.responses import HTMLResponse, JSONResponse, StreamingResponse
16
15
  from fastapi.staticfiles import StaticFiles
17
16
  from fastapi.templating import Jinja2Templates
18
17
 
@@ -88,33 +87,40 @@ async def start(request: Request):
88
87
  """A default start on app."""
89
88
  return templates.TemplateResponse("index.html", {"request": request})
90
89
 
91
- @app.post("/", response_class=RedirectResponse)
90
+ @app.post("/", response_class=JSONResponse)
92
91
  async def upload(request: Request):
93
92
  """Upload the file, then encode or decode depend on selection."""
94
- form = await request.form()
95
- file = form.get("file")
96
-
97
- if not file or not form.get("upload_type"):
98
- return templates.TemplateResponse("index.html",
99
- {"request": request, "error": "Unable to load form."})
93
+ try:
94
+ form = await request.form()
95
+ file = form.get("file")
100
96
 
101
- # Im poor u know lol. You can delete this line, just acknowledge your cpu power.
102
- if file.size > 1_073_741_824:
103
- return templates.TemplateResponse("index.html",
104
- {"request": request, "error": "File should be <1GB."})
97
+ if not file or not form.get("upload_type"):
98
+ return JSONResponse(content={"error": "Unable to load form."})
105
99
 
106
- while True: # Guarantee an individualized uid.
107
- uid = uuid.uuid4().hex
108
- try:
109
- os.mkdir(os.path.join(IMAGE_DIR, uid))
110
- break
111
- except FileExistsError:
112
- continue
100
+ # Im poor u know lol. You can delete this line, just acknowledge your cpu power.
101
+ if file.size > 1_073_741_824:
102
+ return JSONResponse(content={"error": "File should be <1GB."})
113
103
 
114
- upload_type = form.get("upload_type")
115
- with open(_join_uid(uid, f"{upload_type}_input.png"), "wb") as buffer:
116
- shutil.copyfileobj(file.file, buffer)
117
- return RedirectResponse(f"/{upload_type}/{uid}", status_code=303)
104
+ while True: # Guarantee an individualized uid.
105
+ uid = uuid.uuid4().hex
106
+ try:
107
+ os.mkdir(os.path.join(IMAGE_DIR, uid))
108
+ break
109
+ except FileExistsError:
110
+ continue
111
+
112
+ upload_type = form.get("upload_type")
113
+ img = Image.open(file.file)
114
+ img = await asyncio.get_event_loop().run_in_executor(executor, img.convert, "RGB")
115
+ await asyncio.get_event_loop().run_in_executor(executor, img.save,
116
+ _join_uid(uid, f"{upload_type}_input.png"))
117
+ return JSONResponse(content={"redirect": f"/{upload_type}/{uid}"})
118
+ except UnidentifiedImageError:
119
+ return JSONResponse(content={"error": "Unsupported image extension."})
120
+ except Exception as err: #pylint: disable=W0718:broad-exception-caught
121
+ return JSONResponse(content={"error": err})
122
+ except asyncio.exceptions.CancelledError:
123
+ return
118
124
 
119
125
  @app.get("/encode/{uid}", response_class=HTMLResponse)
120
126
  async def start_encode(request: Request, uid: str):
@@ -141,31 +147,39 @@ async def end_encode(request: Request, uid: str):
141
147
  case "panel_preview":
142
148
  if input_path and form.get("intensity"):
143
149
  try:
150
+ generator = Generator(input_path)
144
151
  future_item = asyncio.get_event_loop().run_in_executor(executor,
145
- Generator(input_path).preview, float(form.get("intensity")),
152
+ generator.preview, float(form.get("intensity")),
146
153
  _join_uid(uid, "encode_preview.png"))
154
+ future_item.add_done_callback(lambda _: _remove_from_list(uid))
147
155
  background_task.update({uid: future_item})
148
156
 
149
157
  async def _progress_streaming():
150
- for progress in await future_item:
151
- yield f"{progress}\n"
152
- _remove_from_list(uid)
158
+ while not future_item.done():
159
+ yield f"{generator.get_progress()}\n"
160
+ await asyncio.sleep(1)
153
161
  yield json.dumps({"result": templates.get_template("result.html")
154
162
  .render({"path": f"{uid}/encode_preview.png",
155
163
  "download": "encode-preview",})})
156
164
  return StreamingResponse(_progress_streaming(), media_type="text/plain")
157
165
 
158
166
  except asyncio.exceptions.CancelledError:
159
- return_error = "User exited."
167
+ return
160
168
  except AssertionError as err:
161
169
  return_error = err
162
170
 
163
171
  case "panel_resize":
164
172
  if input_path and form.get("image") and form.get("width") and form.get("length"):
165
173
  def _resize() -> None:
166
- Image.open(form.get("image").file)\
167
- .resize((int(form.get("width")), int(form.get("length"))))\
168
- .save(_join_uid(uid, "encode_resize.png"), optimize=True)
174
+ match form.get("image"):
175
+ case "uploaded":
176
+ img_file = input_path
177
+ case "preview":
178
+ img_file = _join_uid(uid, "encode_preview.png")
179
+ case "custom":
180
+ img_file = form.get("image_file").file
181
+ Image.open(img_file).resize((int(form.get("width")), int(form.get("length"))))\
182
+ .save(_join_uid(uid, "encode_resize.png"), optimize=True)
169
183
  try:
170
184
  future_item=asyncio.get_event_loop().run_in_executor(executor, _resize)
171
185
  background_task.update({uid: future_item})
@@ -175,21 +189,33 @@ async def end_encode(request: Request, uid: str):
175
189
  "path": f"{uid}/encode_resize.png",
176
190
  "download": "encode-resize",
177
191
  })})
192
+ except UnidentifiedImageError:
193
+ return_error = "Unsupported image extension."
178
194
  except asyncio.exceptions.CancelledError:
179
- return_error = "User exited."
195
+ return
180
196
 
181
197
  case "panel_steganography":
182
198
  if input_path and form.get("disguise"):
183
- Image.open(form.get("disguise").file)\
184
- .save(_join_uid(uid, "encode_disguise.png"), optimize=True)
185
- future_item=asyncio.get_event_loop().run_in_executor(executor, Steganography.encode,
186
- form.get("password"), input_path,
187
- _join_uid(uid, "encode_steganography.png"),
188
- _join_uid(uid, "encode_disguise.png"))
189
- future_item.add_done_callback(lambda _: _remove_from_list(uid))
190
- background_task.update({uid: future_item})
191
-
199
+ def _encode() -> str:
200
+ match form.get("disguise"):
201
+ case "uploaded":
202
+ img_file = input_path
203
+ case "preview":
204
+ img_file = _join_uid(uid, "encode_preview.png")
205
+ case "resize":
206
+ img_file = _join_uid(uid, "encode_resize.png")
207
+ case "custom":
208
+ img_file = _join_uid(uid, "encode_disguise.png")
209
+
210
+ img = Image.open(form.get("disguise_file").file)
211
+ img.save(img_file, optimize=True)
212
+ return Steganography.encode(form.get("password"), input_path,
213
+ _join_uid(uid, "encode_steganography.png"), img_file)
192
214
  try:
215
+ future_item=asyncio.get_event_loop().run_in_executor(executor, _encode)
216
+ future_item.add_done_callback(lambda _: _remove_from_list(uid))
217
+ background_task.update({uid: future_item})
218
+
193
219
  if await future_item:
194
220
  return_error = future_item.result()
195
221
  else:
@@ -198,8 +224,10 @@ async def end_encode(request: Request, uid: str):
198
224
  "path": f"{uid}/encode_steganography.png",
199
225
  "download": "encode-steganography",
200
226
  })})
227
+ except UnidentifiedImageError:
228
+ return_error = "Unsupported image extension."
201
229
  except asyncio.exceptions.CancelledError:
202
- return_error = "User exited."
230
+ return
203
231
 
204
232
  case _:
205
233
  return_error = "Unable to load form."
@@ -258,10 +286,10 @@ async def end_decode(request: Request, uid: str):
258
286
  except UnidentifiedImageError:
259
287
  continue
260
288
  return_error = "There is no readable image within the encoded file."
261
- except asyncio.exceptions.CancelledError:
262
- return_error = "User exited."
263
289
  except BaseException as err: #pylint: disable=W0718:broad-exception-caught
264
290
  return_error = f"This error usually occurs when the image is not encoded from here: {err}"
291
+ except asyncio.exceptions.CancelledError:
292
+ return
265
293
  return {"error": templates.get_template("error.html").render({"error": return_error})}
266
294
 
267
295
  @app.post("/remove/{uid}")
@@ -270,12 +298,4 @@ async def remove(uid: str):
270
298
  shutil.rmtree(os.path.join(IMAGE_DIR, uid), ignore_errors=True)
271
299
  task: asyncio.Future = background_task.get(uid)
272
300
  if task:
273
- try:
274
- result = task.result()
275
- if isinstance(result, types.GeneratorType):
276
- result.throw(asyncio.exceptions.CancelledError("Task cancelled."))
277
- _remove_from_list(uid)
278
- else:
279
- task.cancel()
280
- except (asyncio.exceptions.InvalidStateError, asyncio.exceptions.CancelledError):
281
- task.cancel()
301
+ task.cancel()
@@ -34,11 +34,11 @@ wheels = [
34
34
 
35
35
  [[package]]
36
36
  name = "certifi"
37
- version = "2026.1.4"
37
+ version = "2026.2.25"
38
38
  source = { registry = "https://pypi.org/simple" }
39
- sdist = { url = "https://files.pythonhosted.org/packages/e0/2d/a891ca51311197f6ad14a7ef42e2399f36cf2f9bd44752b3dc4eab60fdc5/certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120", size = 154268, upload-time = "2026-01-04T02:42:41.825Z" }
39
+ sdist = { url = "https://files.pythonhosted.org/packages/af/2d/7bf41579a8986e348fa033a31cdd0e4121114f6bce2457e8876010b092dd/certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7", size = 155029, upload-time = "2026-02-25T02:54:17.342Z" }
40
40
  wheels = [
41
- { url = "https://files.pythonhosted.org/packages/e6/ad/3cc14f097111b4de0040c83a525973216457bbeeb63739ef1ed275c1c021/certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c", size = 152900, upload-time = "2026-01-04T02:42:40.15Z" },
41
+ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" },
42
42
  ]
43
43
 
44
44
  [[package]]
@@ -125,7 +125,7 @@ wheels = [
125
125
 
126
126
  [[package]]
127
127
  name = "fastapi"
128
- version = "0.131.0"
128
+ version = "0.133.1"
129
129
  source = { registry = "https://pypi.org/simple" }
130
130
  dependencies = [
131
131
  { name = "annotated-doc" },
@@ -134,9 +134,9 @@ dependencies = [
134
134
  { name = "typing-extensions" },
135
135
  { name = "typing-inspection" },
136
136
  ]
137
- sdist = { url = "https://files.pythonhosted.org/packages/91/32/158cbf685b7d5a26f87131069da286bf10fc9fbf7fc968d169d48a45d689/fastapi-0.131.0.tar.gz", hash = "sha256:6531155e52bee2899a932c746c9a8250f210e3c3303a5f7b9f8a808bfe0548ff", size = 369612, upload-time = "2026-02-22T16:38:11.252Z" }
137
+ sdist = { url = "https://files.pythonhosted.org/packages/22/6f/0eafed8349eea1fa462238b54a624c8b408cd1ba2795c8e64aa6c34f8ab7/fastapi-0.133.1.tar.gz", hash = "sha256:ed152a45912f102592976fde6cbce7dae1a8a1053da94202e51dd35d184fadd6", size = 378741, upload-time = "2026-02-25T18:18:17.398Z" }
138
138
  wheels = [
139
- { url = "https://files.pythonhosted.org/packages/ff/94/b58ec24c321acc2ad1327f69b033cadc005e0f26df9a73828c9e9c7db7ce/fastapi-0.131.0-py3-none-any.whl", hash = "sha256:ed0e53decccf4459de78837ce1b867cd04fa9ce4579497b842579755d20b405a", size = 103854, upload-time = "2026-02-22T16:38:09.814Z" },
139
+ { url = "https://files.pythonhosted.org/packages/d2/c9/a175a7779f3599dfa4adfc97a6ce0e157237b3d7941538604aadaf97bfb6/fastapi-0.133.1-py3-none-any.whl", hash = "sha256:658f34ba334605b1617a65adf2ea6461901bdb9af3a3080d63ff791ecf7dc2e2", size = 109029, upload-time = "2026-02-25T18:18:18.578Z" },
140
140
  ]
141
141
 
142
142
  [package.optional-dependencies]
@@ -153,16 +153,16 @@ standard = [
153
153
 
154
154
  [[package]]
155
155
  name = "fastapi-cli"
156
- version = "0.0.23"
156
+ version = "0.0.24"
157
157
  source = { registry = "https://pypi.org/simple" }
158
158
  dependencies = [
159
159
  { name = "rich-toolkit" },
160
160
  { name = "typer" },
161
161
  { name = "uvicorn", extra = ["standard"] },
162
162
  ]
163
- sdist = { url = "https://files.pythonhosted.org/packages/71/9f/cbd463e57de4e977b8ea0403f95347f9150441568b1d3fe3e4949ef80ef3/fastapi_cli-0.0.23.tar.gz", hash = "sha256:210ac280ea41e73aac5a57688781256beb23c2cba3a41266896fa43e6445c8e7", size = 19763, upload-time = "2026-02-16T19:45:53.358Z" }
163
+ sdist = { url = "https://files.pythonhosted.org/packages/6e/58/74797ae9e4610cfa0c6b34c8309096d3b20bb29be3b8b5fbf1004d10fa5f/fastapi_cli-0.0.24.tar.gz", hash = "sha256:1afc9c9e21d7ebc8a3ca5e31790cd8d837742be7e4f8b9236e99cb3451f0de00", size = 19043, upload-time = "2026-02-24T10:45:10.476Z" }
164
164
  wheels = [
165
- { url = "https://files.pythonhosted.org/packages/68/89/19dcfd5cd289b306abdcabac68b88a4f54b7710a2c33adc16a337ecdcdfa/fastapi_cli-0.0.23-py3-none-any.whl", hash = "sha256:7e9634fc212da0b6cfc75bd3ac366cc9dfdb43b5e9ec12e58bfd1acdd2697f25", size = 12305, upload-time = "2026-02-16T19:45:52.554Z" },
165
+ { url = "https://files.pythonhosted.org/packages/c7/4b/68f9fe268e535d79c76910519530026a4f994ce07189ac0dded45c6af825/fastapi_cli-0.0.24-py3-none-any.whl", hash = "sha256:4a1f78ed798f106b4fee85ca93b85d8fe33c0a3570f775964d37edb80b8f0edc", size = 12304, upload-time = "2026-02-24T10:45:09.552Z" },
166
166
  ]
167
167
 
168
168
  [package.optional-dependencies]
@@ -173,7 +173,7 @@ standard = [
173
173
 
174
174
  [[package]]
175
175
  name = "fastapi-cloud-cli"
176
- version = "0.13.0"
176
+ version = "0.14.0"
177
177
  source = { registry = "https://pypi.org/simple" }
178
178
  dependencies = [
179
179
  { name = "fastar" },
@@ -185,9 +185,9 @@ dependencies = [
185
185
  { name = "typer" },
186
186
  { name = "uvicorn", extra = ["standard"] },
187
187
  ]
188
- sdist = { url = "https://files.pythonhosted.org/packages/de/0b/f07f4976784978ef159fd2e8f5c16f1f9d610578fb1fd976ff1315c11ea6/fastapi_cloud_cli-0.13.0.tar.gz", hash = "sha256:4d8f42337e8021c648f6cb0672de7d5b31b0fc7387a83d7b12f974600ac3f2fd", size = 38436, upload-time = "2026-02-17T05:18:19.033Z" }
188
+ sdist = { url = "https://files.pythonhosted.org/packages/2b/eb/e78ebd05a714c62a0578cdce4339cb6cd138421a7d865fbddedd7242420b/fastapi_cloud_cli-0.14.0.tar.gz", hash = "sha256:d3ecb8c942685a71df0af7bd59f463b5eff76f5818b48e5a03c6159726831e68", size = 39822, upload-time = "2026-02-25T14:19:53.535Z" }
189
189
  wheels = [
190
- { url = "https://files.pythonhosted.org/packages/b4/88/71a1e989d17b9edb483f32e28b7891ffdd3005271518c98ba6415987c430/fastapi_cloud_cli-0.13.0-py3-none-any.whl", hash = "sha256:874a9ed8dba34ec828f198c72de9f9a38de77ac1b15083d6bc3a4d772b0bc477", size = 27631, upload-time = "2026-02-17T05:18:18.094Z" },
190
+ { url = "https://files.pythonhosted.org/packages/d9/18/7bf922ee0b6a737a9d88cf613182ecd6031f52298da893556f158eba763f/fastapi_cloud_cli-0.14.0-py3-none-any.whl", hash = "sha256:325fcb4b45e661184152da6db861d9fb718739fbcd561a4d334dbe78c026586f", size = 28350, upload-time = "2026-02-25T14:19:52.416Z" },
191
191
  ]
192
192
 
193
193
  [[package]]
@@ -653,16 +653,16 @@ wheels = [
653
653
 
654
654
  [[package]]
655
655
  name = "rich-toolkit"
656
- version = "0.19.4"
656
+ version = "0.19.7"
657
657
  source = { registry = "https://pypi.org/simple" }
658
658
  dependencies = [
659
659
  { name = "click" },
660
660
  { name = "rich" },
661
661
  { name = "typing-extensions" },
662
662
  ]
663
- sdist = { url = "https://files.pythonhosted.org/packages/d0/c9/4bbf4bfee195ed1b7d7a6733cc523ca61dbfb4a3e3c12ea090aaffd97597/rich_toolkit-0.19.4.tar.gz", hash = "sha256:52e23d56f9dc30d1343eb3b3f6f18764c313fbfea24e52e6a1d6069bec9c18eb", size = 193951, upload-time = "2026-02-12T10:08:15.814Z" }
663
+ sdist = { url = "https://files.pythonhosted.org/packages/42/ba/dae9e3096651042754da419a4042bc1c75e07d615f9b15066d738838e4df/rich_toolkit-0.19.7.tar.gz", hash = "sha256:133c0915872da91d4c25d85342d5ec1dfacc69b63448af1a08a0d4b4f23ef46e", size = 195877, upload-time = "2026-02-24T16:06:20.555Z" }
664
664
  wheels = [
665
- { url = "https://files.pythonhosted.org/packages/28/31/97d39719def09c134385bfcfbedfed255168b571e7beb3ad7765aae660ca/rich_toolkit-0.19.4-py3-none-any.whl", hash = "sha256:34ac344de8862801644be8b703e26becf44b047e687f208d7829e8f7cfc311d6", size = 32757, upload-time = "2026-02-12T10:08:15.037Z" },
665
+ { url = "https://files.pythonhosted.org/packages/fb/3c/c923619f6d2f5fafcc96fec0aaf9550a46cd5b6481f06e0c6b66a2a4fed0/rich_toolkit-0.19.7-py3-none-any.whl", hash = "sha256:0288e9203728c47c5a4eb60fd2f0692d9df7455a65901ab6f898437a2ba5989d", size = 32963, upload-time = "2026-02-24T16:06:22.066Z" },
666
666
  ]
667
667
 
668
668
  [[package]]
@@ -1,79 +0,0 @@
1
- # cspell:ignore setdiff, fromarray
2
- """
3
- Generating layers from an image file.
4
- """
5
-
6
- import logging
7
- import os
8
- from collections.abc import Generator as gen
9
- import asyncio
10
- from random import shuffle, randint
11
-
12
- import numpy as np
13
- from PIL import Image
14
-
15
- class Generator:
16
- """
17
- The generator.
18
- """
19
- def __init__(self, input_path: str):
20
- try: # convert to RGB to remove the only way AI can learn to solve.
21
- self.img_data = np.array(Image.open(input_path).convert("RGB"))
22
- except Exception as e:
23
- logging.error("Error opening image: %s", e)
24
- raise e
25
- self._allowance: int = -1
26
- self._remain_allowance: int = -1
27
-
28
- def preview(self, intensity: float, output_path: str) -> gen[int]:
29
- """
30
- Generate the preview layer AND STREAM PROGRESS OUT.
31
- ```
32
- generator = Generator("Path/to/image.png")
33
- for progress in generator.preview(0.5, "Path/to/output/image.png"):
34
- print(progress)
35
- ```
36
-
37
-
38
- Args:
39
- intensity (float): the amount of pixel being taken (0-1). The smaller it is, the \
40
- faster to process, the harder to visualize. (AI may have the stroke, and so \
41
- human's eyes)
42
- output_path (str): Where the file should be saved at.
43
-
44
- Returns:
45
- collections.abc.Generator[int]: The percentage of completion.
46
- """
47
- assert 0 <= intensity <= 1, "Invalid intensity"
48
-
49
- try:
50
- # [[i, j] for i in range(img_data.shape[0]) for j in range(img_data.shape[1])]
51
- available_location = np.stack(np.meshgrid(np.arange(self.img_data.shape[0]),
52
- np.arange(self.img_data.shape[1])), axis=-1).reshape(-1, 2).tolist()
53
- shuffle(available_location)
54
-
55
- # do the shit.
56
- self._allowance = int(int(np.sum(self.img_data) * intensity))
57
- self._remain_allowance = self._allowance
58
-
59
- layer: np.ndarray = np.zeros_like(self.img_data)
60
- while self._remain_allowance > 0:
61
- try:
62
- location = tuple(available_location.pop())
63
- except IndexError:
64
- break
65
- for current_value in range(3): # RGBA channels
66
- value=min (randint(0, self.img_data[location][current_value]),
67
- self._remain_allowance)
68
- layer[location][current_value] = value
69
- self._remain_allowance -= value
70
- yield int(100*(1-self._remain_allowance/self._allowance))
71
- os.makedirs(os.path.dirname(output_path), exist_ok=True)
72
- Image.fromarray(layer.astype(np.uint8), "RGB").save(output_path)
73
- except asyncio.exceptions.CancelledError:
74
- yield -1
75
-
76
- if __name__ == "__main__":
77
- generator = Generator("assets/material.png")
78
- for progress in generator.preview(0.5, "assets/preview.png"):
79
- print(progress)
File without changes
File without changes
File without changes
File without changes