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.
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/Cargo.lock +1 -1
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/Cargo.toml +1 -3
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/PKG-INFO +5 -4
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/README.md +1 -1
- {quantum_pixel-2.0.4/quantum_pixel → quantum_pixel-2.2.0}/README_PYPI.md +1 -1
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/pyproject.toml +9 -3
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0/quantum_pixel}/README_PYPI.md +1 -1
- quantum_pixel-2.2.0/quantum_pixel/generator.py +74 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/main.py +4 -1
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/decode.js +9 -1
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/encode.js +35 -6
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/styles.css +2 -2
- quantum_pixel-2.2.0/quantum_pixel/static/upload.js +36 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/decode.html +2 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/encode.html +37 -4
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/index.html +6 -5
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/web.py +74 -54
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/uv.lock +15 -15
- quantum_pixel-2.0.4/quantum_pixel/generator.py +0 -79
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.github/FUNDING.yml +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.github/workflows/CI.yml +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.github/workflows/render-md-mermaid.yml +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.gitignore +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/.python-version +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/LICENSE +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/encoded.png +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/grok.png +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/intro.png +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/material.png +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/mermaid.png +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/assets/preview.png +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/__init__.py +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/favicon.svg +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/static/robots.txt +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/steganography.py +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/error.html +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/quantum_pixel/templates/result.html +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/render.yaml +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/requirements.txt +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/src/lib.rs +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/tests/test_generator.py +0 -0
- {quantum_pixel-2.0.4 → quantum_pixel-2.2.0}/tests/test_steganography.py +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
edition = "2024"
|
|
3
3
|
name = "quantum-pixel"
|
|
4
|
-
version = "2.0
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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:
|
|
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
|
+
})();
|
|
@@ -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="
|
|
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="
|
|
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
|
-
|
|
15
|
-
<div class="error">{{ error }}</div>
|
|
16
|
-
{% endif %}
|
|
14
|
+
<div id="error" style="display: none;"></div>
|
|
17
15
|
|
|
18
|
-
<form class="upload-form"
|
|
19
|
-
<input type="file" name="file" accept="image
|
|
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,
|
|
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=
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
102
|
-
|
|
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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
-
yield f"{
|
|
152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
195
|
+
return
|
|
180
196
|
|
|
181
197
|
case "panel_steganography":
|
|
182
198
|
if input_path and form.get("disguise"):
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
37
|
+
version = "2026.2.25"
|
|
38
38
|
source = { registry = "https://pypi.org/simple" }
|
|
39
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
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/
|
|
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.
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
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/
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|