Pixseal 0.2.2__pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl
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.
- Pixseal/__init__.py +26 -0
- Pixseal/imageSigner.py +206 -0
- Pixseal/imageValidator.py +221 -0
- Pixseal/simpleImage.py +35 -0
- Pixseal/simpleImage_ext.pypy38-pp73-x86-linux-gnu.so +0 -0
- Pixseal/simpleImage_py.py +748 -0
- pixseal-0.2.2.dist-info/METADATA +240 -0
- pixseal-0.2.2.dist-info/RECORD +10 -0
- pixseal-0.2.2.dist-info/WHEEL +8 -0
- pixseal-0.2.2.dist-info/top_level.txt +1 -0
Pixseal/__init__.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from .simpleImage import ImageInput, SimpleImage
|
|
2
|
+
from .imageSigner import (
|
|
3
|
+
BinaryProvider,
|
|
4
|
+
addHiddenBit,
|
|
5
|
+
signImage,
|
|
6
|
+
)
|
|
7
|
+
from .imageValidator import (
|
|
8
|
+
binaryToString,
|
|
9
|
+
buildValidationReport,
|
|
10
|
+
deduplicate,
|
|
11
|
+
readHiddenBit,
|
|
12
|
+
validateImage,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__all__ = [
|
|
16
|
+
"SimpleImage",
|
|
17
|
+
"ImageInput",
|
|
18
|
+
"BinaryProvider",
|
|
19
|
+
"addHiddenBit",
|
|
20
|
+
"signImage",
|
|
21
|
+
"binaryToString",
|
|
22
|
+
"buildValidationReport",
|
|
23
|
+
"deduplicate",
|
|
24
|
+
"readHiddenBit",
|
|
25
|
+
"validateImage",
|
|
26
|
+
]
|
Pixseal/imageSigner.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
import base64
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
6
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
7
|
+
|
|
8
|
+
# profiler check
|
|
9
|
+
try:
|
|
10
|
+
from line_profiler import profile
|
|
11
|
+
except ImportError:
|
|
12
|
+
|
|
13
|
+
def profile(func):
|
|
14
|
+
return func
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Dynamic typing
|
|
18
|
+
from .simpleImage import (
|
|
19
|
+
ImageInput as _RuntimeImageInput,
|
|
20
|
+
SimpleImage as _RuntimeSimpleImage,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .simpleImage_py import ImageInput, SimpleImage
|
|
25
|
+
else:
|
|
26
|
+
ImageInput = _RuntimeImageInput
|
|
27
|
+
SimpleImage = _RuntimeSimpleImage
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BinaryProvider:
|
|
31
|
+
|
|
32
|
+
# Constructor
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
hiddenString,
|
|
36
|
+
startString="START-VALIDATION\n",
|
|
37
|
+
endString="\nEND-VALIDATION",
|
|
38
|
+
):
|
|
39
|
+
self.hiddenBits = self._stringToBits(hiddenString)
|
|
40
|
+
self.startBits = self._stringToBits(startString)
|
|
41
|
+
self.endBits = self._stringToBits(endString)
|
|
42
|
+
|
|
43
|
+
# Convert string to contiguous binary digits
|
|
44
|
+
def _stringToBits(self, string):
|
|
45
|
+
bits = []
|
|
46
|
+
for char in string:
|
|
47
|
+
binary = format(ord(char), "08b")
|
|
48
|
+
bits.extend(int(bit) for bit in binary)
|
|
49
|
+
return bits
|
|
50
|
+
|
|
51
|
+
def _expandPayload(self, count: int):
|
|
52
|
+
if count <= 0:
|
|
53
|
+
return []
|
|
54
|
+
payloadLen = len(self.hiddenBits)
|
|
55
|
+
if payloadLen == 0:
|
|
56
|
+
raise ValueError("Hidden payload is empty")
|
|
57
|
+
repeats, remainder = divmod(count, payloadLen)
|
|
58
|
+
return (self.hiddenBits * repeats) + self.hiddenBits[:remainder]
|
|
59
|
+
|
|
60
|
+
def buildBitArray(self, pixelCount: int):
|
|
61
|
+
startLen = len(self.startBits)
|
|
62
|
+
endLen = len(self.endBits)
|
|
63
|
+
if pixelCount < startLen + endLen:
|
|
64
|
+
raise ValueError("Image is too small to fit start/end sentinels")
|
|
65
|
+
|
|
66
|
+
bits = [0] * pixelCount
|
|
67
|
+
|
|
68
|
+
# 1. Place START-VALIDATION bits first
|
|
69
|
+
bits[:startLen] = self.startBits
|
|
70
|
+
|
|
71
|
+
# 2. Fill the remaining slots by repeating the payload bits
|
|
72
|
+
payloadSlots = pixelCount - startLen
|
|
73
|
+
payloadBits = self._expandPayload(payloadSlots)
|
|
74
|
+
bits[startLen:] = payloadBits[:payloadSlots]
|
|
75
|
+
|
|
76
|
+
# 3. Overwrite the tail with END-VALIDATION bits
|
|
77
|
+
tailStart = pixelCount - endLen
|
|
78
|
+
bits[tailStart:] = self.endBits
|
|
79
|
+
|
|
80
|
+
return bits
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@profile
|
|
84
|
+
def addHiddenBit(imageInput: ImageInput, hiddenBinary: BinaryProvider):
|
|
85
|
+
img = SimpleImage.open(imageInput)
|
|
86
|
+
width, height = img.size
|
|
87
|
+
pixels = img._pixels # direct buffer access for performance
|
|
88
|
+
total = width * height
|
|
89
|
+
payloadBits = hiddenBinary.buildBitArray(total)
|
|
90
|
+
|
|
91
|
+
# Iterate over every pixel and inject one bit
|
|
92
|
+
for idx in range(total):
|
|
93
|
+
base = idx * 3
|
|
94
|
+
# Read the pixel
|
|
95
|
+
r = pixels[base]
|
|
96
|
+
g = pixels[base + 1]
|
|
97
|
+
b = pixels[base + 2]
|
|
98
|
+
|
|
99
|
+
# Calculate the distance from 127
|
|
100
|
+
diffR = r - 127
|
|
101
|
+
if diffR < 0:
|
|
102
|
+
diffR = -diffR
|
|
103
|
+
diffG = g - 127
|
|
104
|
+
if diffG < 0:
|
|
105
|
+
diffG = -diffG
|
|
106
|
+
diffB = b - 127
|
|
107
|
+
if diffB < 0:
|
|
108
|
+
diffB = -diffB
|
|
109
|
+
|
|
110
|
+
# Pick the component farthest from 127
|
|
111
|
+
maxDiff = diffR
|
|
112
|
+
if diffG > maxDiff:
|
|
113
|
+
maxDiff = diffG
|
|
114
|
+
if diffB > maxDiff:
|
|
115
|
+
maxDiff = diffB
|
|
116
|
+
|
|
117
|
+
# Actual value of that channel
|
|
118
|
+
if maxDiff == diffR:
|
|
119
|
+
targetColorValue = r
|
|
120
|
+
elif maxDiff == diffG:
|
|
121
|
+
targetColorValue = g
|
|
122
|
+
else:
|
|
123
|
+
targetColorValue = b
|
|
124
|
+
|
|
125
|
+
# Channels >=127 are decremented, <127 incremented
|
|
126
|
+
addDirection = 1 if targetColorValue < 127 else -1
|
|
127
|
+
|
|
128
|
+
# Pull next bit from provider
|
|
129
|
+
bit = payloadBits[idx]
|
|
130
|
+
|
|
131
|
+
# Force the selected channel parity to match the bit
|
|
132
|
+
if maxDiff == diffR:
|
|
133
|
+
if r % 2 != bit:
|
|
134
|
+
r += addDirection
|
|
135
|
+
if maxDiff == diffG:
|
|
136
|
+
if g % 2 != bit:
|
|
137
|
+
g += addDirection
|
|
138
|
+
if maxDiff == diffB:
|
|
139
|
+
if b % 2 != bit:
|
|
140
|
+
b += addDirection
|
|
141
|
+
|
|
142
|
+
# Write the updated pixel
|
|
143
|
+
pixels[base] = r
|
|
144
|
+
pixels[base + 1] = g
|
|
145
|
+
pixels[base + 2] = b
|
|
146
|
+
|
|
147
|
+
# Return the modified image
|
|
148
|
+
return img
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def stringCryptor(plaintext: str, public_key) -> str:
|
|
152
|
+
|
|
153
|
+
ciphertext = public_key.encrypt(
|
|
154
|
+
plaintext.encode("utf-8"),
|
|
155
|
+
padding.OAEP(
|
|
156
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
157
|
+
algorithm=hashes.SHA256(),
|
|
158
|
+
label=None,
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
return base64.b64encode(ciphertext).decode("ascii")
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# main
|
|
166
|
+
# Image input (path or bytes) + payload string => returns image with embedded payload
|
|
167
|
+
def signImage(imageInput: ImageInput, hiddenString, publicKeyPath=None):
|
|
168
|
+
"""
|
|
169
|
+
Embed a payload into an image using the parity-based steganography scheme.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
imageInput: File path, bytes, or file-like object accepted by SimpleImage.
|
|
173
|
+
hiddenString: Text payload that should be written into the image.
|
|
174
|
+
publicKeyPath: Optional path to a PEM-encoded RSA public key used to
|
|
175
|
+
encrypt the payload and sentinel markers before embedding.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
SimpleImage instance whose pixels include the signed payload.
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
FileNotFoundError: If a public key path is provided but the file is missing.
|
|
182
|
+
ValueError: If the file is not a valid PEM public key.
|
|
183
|
+
"""
|
|
184
|
+
|
|
185
|
+
if publicKeyPath: # When encryption key is supplied
|
|
186
|
+
key_path = Path(publicKeyPath)
|
|
187
|
+
if not key_path.is_file():
|
|
188
|
+
raise FileNotFoundError(f"Public key file not found: {publicKeyPath}")
|
|
189
|
+
|
|
190
|
+
pem_data = key_path.read_bytes()
|
|
191
|
+
if b"BEGIN PUBLIC KEY" not in pem_data:
|
|
192
|
+
raise ValueError("Provided file does not contain a valid public key")
|
|
193
|
+
|
|
194
|
+
public_key = serialization.load_pem_public_key(pem_data)
|
|
195
|
+
|
|
196
|
+
hiddenBinary = BinaryProvider(
|
|
197
|
+
hiddenString=stringCryptor(hiddenString, public_key) + "\n",
|
|
198
|
+
startString=stringCryptor("START-VALIDATION", public_key) + "\n",
|
|
199
|
+
endString="\n" + stringCryptor("END-VALIDATION", public_key),
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
else: # Plain-text payload
|
|
203
|
+
hiddenBinary = BinaryProvider(hiddenString + "\n")
|
|
204
|
+
|
|
205
|
+
signedImage = addHiddenBit(imageInput, hiddenBinary)
|
|
206
|
+
return signedImage
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
from typing import TYPE_CHECKING
|
|
3
|
+
|
|
4
|
+
from cryptography.hazmat.primitives import hashes, serialization
|
|
5
|
+
from cryptography.hazmat.primitives.asymmetric import padding
|
|
6
|
+
|
|
7
|
+
# profiler check
|
|
8
|
+
try:
|
|
9
|
+
from line_profiler import profile
|
|
10
|
+
except ImportError:
|
|
11
|
+
|
|
12
|
+
def profile(func):
|
|
13
|
+
return func
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
# Dynamic typing
|
|
17
|
+
from .simpleImage import (
|
|
18
|
+
ImageInput as _RuntimeImageInput,
|
|
19
|
+
SimpleImage as _RuntimeSimpleImage,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from .simpleImage_py import ImageInput, SimpleImage
|
|
24
|
+
else:
|
|
25
|
+
ImageInput = _RuntimeImageInput
|
|
26
|
+
SimpleImage = _RuntimeSimpleImage
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def binaryToString(binaryCode):
|
|
30
|
+
string = []
|
|
31
|
+
for i in range(0, len(binaryCode), 8):
|
|
32
|
+
byte = binaryCode[i : i + 8]
|
|
33
|
+
decimal = int(byte, 2)
|
|
34
|
+
character = chr(decimal)
|
|
35
|
+
string.append(character)
|
|
36
|
+
return "".join(string)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@profile
|
|
40
|
+
def readHiddenBit(imageInput: ImageInput):
|
|
41
|
+
img = SimpleImage.open(imageInput)
|
|
42
|
+
width, height = img.size
|
|
43
|
+
pixels = img._pixels # direct buffer access for performance
|
|
44
|
+
total = width * height
|
|
45
|
+
bits = []
|
|
46
|
+
append_bit = bits.append
|
|
47
|
+
|
|
48
|
+
for idx in range(total):
|
|
49
|
+
base = idx * 3
|
|
50
|
+
r = pixels[base]
|
|
51
|
+
g = pixels[base + 1]
|
|
52
|
+
b = pixels[base + 2]
|
|
53
|
+
|
|
54
|
+
diffR = r - 127
|
|
55
|
+
if diffR < 0:
|
|
56
|
+
diffR = -diffR
|
|
57
|
+
diffG = g - 127
|
|
58
|
+
if diffG < 0:
|
|
59
|
+
diffG = -diffG
|
|
60
|
+
diffB = b - 127
|
|
61
|
+
if diffB < 0:
|
|
62
|
+
diffB = -diffB
|
|
63
|
+
|
|
64
|
+
maxDiff = diffR
|
|
65
|
+
if diffG > maxDiff:
|
|
66
|
+
maxDiff = diffG
|
|
67
|
+
if diffB > maxDiff:
|
|
68
|
+
maxDiff = diffB
|
|
69
|
+
|
|
70
|
+
append_bit("1" if maxDiff % 2 == 0 else "0")
|
|
71
|
+
|
|
72
|
+
return "".join(bits)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def deduplicate(arr):
|
|
76
|
+
deduplicated = []
|
|
77
|
+
freq = {}
|
|
78
|
+
most_common = None
|
|
79
|
+
most_count = 0
|
|
80
|
+
|
|
81
|
+
for i, value in enumerate(arr):
|
|
82
|
+
freq[value] = freq.get(value, 0) + 1
|
|
83
|
+
if freq[value] > most_count:
|
|
84
|
+
most_count = freq[value]
|
|
85
|
+
most_common = value
|
|
86
|
+
|
|
87
|
+
if i == 0 or value != arr[i - 1]:
|
|
88
|
+
deduplicated.append(value)
|
|
89
|
+
|
|
90
|
+
return deduplicated, most_common
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def tailCheck(arr: list[str]):
|
|
94
|
+
if len(arr) != 4:
|
|
95
|
+
return None # Not required
|
|
96
|
+
|
|
97
|
+
full_cipher = arr[1] # complete ciphertext
|
|
98
|
+
truncated_cipher = arr[2] # incomplete ciphertext
|
|
99
|
+
|
|
100
|
+
return full_cipher.startswith(truncated_cipher)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def buildValidationReport(decrypted, tailCheck: bool, skipPlain: bool = False):
|
|
104
|
+
# Length after deduplication/decryption
|
|
105
|
+
arrayLength = len(decrypted)
|
|
106
|
+
|
|
107
|
+
# 1. Check that the deduplicated sequence length is valid
|
|
108
|
+
lengthCheck = arrayLength in (3, 4)
|
|
109
|
+
|
|
110
|
+
# 2. Validate start/end markers
|
|
111
|
+
startCheck = decrypted[0] == "START-VALIDATION" if decrypted else False
|
|
112
|
+
endCheck = decrypted[-1] == "END-VALIDATION" if decrypted else False
|
|
113
|
+
|
|
114
|
+
# 4. Determine whether payload was successfully decrypted
|
|
115
|
+
decryptedPayload = decrypted[1] if len(decrypted) > 1 else ""
|
|
116
|
+
isDecrypted = bool(decryptedPayload) and not decryptedPayload.endswith("==")
|
|
117
|
+
|
|
118
|
+
checkList = [lengthCheck, startCheck, endCheck, isDecrypted]
|
|
119
|
+
# 5. Parse tailCheck result
|
|
120
|
+
if tailCheck is None:
|
|
121
|
+
tailCheckResult = "Not Required"
|
|
122
|
+
else:
|
|
123
|
+
tailCheckResult = tailCheck
|
|
124
|
+
checkList.append(tailCheckResult)
|
|
125
|
+
|
|
126
|
+
# Overall verdict requires every check to pass
|
|
127
|
+
verdict = all(checkList)
|
|
128
|
+
|
|
129
|
+
result = {
|
|
130
|
+
"arrayLength": arrayLength,
|
|
131
|
+
"lengthCheck": lengthCheck,
|
|
132
|
+
"startCheck": startCheck,
|
|
133
|
+
"endCheck": endCheck,
|
|
134
|
+
"isDecrypted": isDecrypted,
|
|
135
|
+
"tailCheckResult": tailCheckResult,
|
|
136
|
+
"verdict": verdict,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if skipPlain:
|
|
140
|
+
result["decryptSkipMessage"] = (
|
|
141
|
+
"Skip decrypt: payload was plain or corrupted text despite decrypt request."
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def decrypt_array(deduplicated, privKeyPath):
|
|
148
|
+
# Load PEM private key
|
|
149
|
+
with open(privKeyPath, "rb") as key_file:
|
|
150
|
+
private_key = serialization.load_pem_private_key(
|
|
151
|
+
key_file.read(),
|
|
152
|
+
password=None,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
decrypted = []
|
|
156
|
+
skippedPlainCount = 0
|
|
157
|
+
decryptError = False
|
|
158
|
+
for item in deduplicated:
|
|
159
|
+
if item.endswith("=="):
|
|
160
|
+
try:
|
|
161
|
+
cipher_bytes = base64.b64decode(item)
|
|
162
|
+
plain_bytes = private_key.decrypt(
|
|
163
|
+
cipher_bytes,
|
|
164
|
+
padding.OAEP(
|
|
165
|
+
mgf=padding.MGF1(algorithm=hashes.SHA256()),
|
|
166
|
+
algorithm=hashes.SHA256(),
|
|
167
|
+
label=None,
|
|
168
|
+
),
|
|
169
|
+
)
|
|
170
|
+
decrypted.append(plain_bytes.decode("utf-8"))
|
|
171
|
+
except Exception as exc:
|
|
172
|
+
print(exc)
|
|
173
|
+
decryptError = True
|
|
174
|
+
decrypted.append(item)
|
|
175
|
+
else:
|
|
176
|
+
skippedPlainCount += 1
|
|
177
|
+
decrypted.append(item)
|
|
178
|
+
|
|
179
|
+
expectedPlainCount = 0
|
|
180
|
+
if len(deduplicated) == 4:
|
|
181
|
+
expectedPlainCount = 1
|
|
182
|
+
|
|
183
|
+
skippedPlain = decryptError or skippedPlainCount != expectedPlainCount
|
|
184
|
+
|
|
185
|
+
return decrypted, skippedPlain
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
# main
|
|
189
|
+
def validateImage(imageInput: ImageInput, privKeyPath=None):
|
|
190
|
+
"""
|
|
191
|
+
Extract the embedded payload from an image and optionally decrypt it.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
imageInput: File path, bytes, or file-like object accepted by SimpleImage.
|
|
195
|
+
privKeyPath: Optional path to a PEM-encoded RSA private key used to
|
|
196
|
+
decrypt the extracted ciphertext.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
Dict with the most common extracted string, decrypted sequence, and
|
|
200
|
+
a validation report describing the sentinel checks and verdict.
|
|
201
|
+
"""
|
|
202
|
+
resultBinary = readHiddenBit(imageInput)
|
|
203
|
+
resultString = binaryToString(resultBinary)
|
|
204
|
+
splited = resultString.split("\n")
|
|
205
|
+
deduplicated, most_common = deduplicate(splited)
|
|
206
|
+
|
|
207
|
+
if privKeyPath:
|
|
208
|
+
decrypted, skippedPlain = decrypt_array(deduplicated, privKeyPath)
|
|
209
|
+
else:
|
|
210
|
+
decrypted = deduplicated
|
|
211
|
+
skippedPlain = False
|
|
212
|
+
|
|
213
|
+
report = buildValidationReport(
|
|
214
|
+
decrypted=decrypted, tailCheck=tailCheck(deduplicated), skipPlain=skippedPlain
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
"extractedString": decrypt_array({most_common}, privKeyPath)[0][0],
|
|
219
|
+
"decrypted": decrypted,
|
|
220
|
+
"validationReport": report,
|
|
221
|
+
}
|
Pixseal/simpleImage.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Loader that prefers the Cython implementation and falls back to pure Python.
|
|
3
|
+
Use environment variable PIXSEAL_SIMPLEIMAGE_BACKEND to force a backend:
|
|
4
|
+
- "cython": require compiled extension
|
|
5
|
+
- "python": force pure Python implementation
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from os import getenv
|
|
9
|
+
|
|
10
|
+
_backend = getenv("PIXSEAL_SIMPLEIMAGE_BACKEND", "auto").lower()
|
|
11
|
+
_loaded_module = "Pixseal.simpleImage_py"
|
|
12
|
+
|
|
13
|
+
if _backend == "python":
|
|
14
|
+
from . import simpleImage_py as _impl # type: ignore
|
|
15
|
+
elif _backend == "cython":
|
|
16
|
+
try:
|
|
17
|
+
from . import simpleImage_ext as _impl # type: ignore
|
|
18
|
+
except ImportError as exc: # pragma: no cover
|
|
19
|
+
raise ImportError(
|
|
20
|
+
"Cython backend requested but Pixseal.simpleImage_ext is not available. "
|
|
21
|
+
"Build the extension or choose the python backend."
|
|
22
|
+
) from exc
|
|
23
|
+
else:
|
|
24
|
+
try: # pragma: no cover
|
|
25
|
+
from . import simpleImage_ext as _impl # type: ignore
|
|
26
|
+
except ImportError: # pragma: no cover
|
|
27
|
+
from . import simpleImage_py as _impl # type: ignore
|
|
28
|
+
|
|
29
|
+
SimpleImage = _impl.SimpleImage # type: ignore
|
|
30
|
+
ImageInput = _impl.ImageInput # type: ignore
|
|
31
|
+
_loaded_module = _impl.__name__
|
|
32
|
+
|
|
33
|
+
print(f"[SimpleImage] Loaded from: {_loaded_module}")
|
|
34
|
+
|
|
35
|
+
__all__ = ["SimpleImage", "ImageInput"]
|
|
Binary file
|