Pixseal 0.2.1.post1__cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.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.

Potentially problematic release.


This version of Pixseal might be problematic. Click here for more details.

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,191 @@
1
+ import base64
2
+ from pathlib import Path
3
+ from cryptography.hazmat.primitives import hashes, serialization
4
+ from cryptography.hazmat.primitives.asymmetric import padding
5
+
6
+ try:
7
+ from line_profiler import profile # type: ignore
8
+ except ImportError: # pragma: no cover
9
+ def profile(func):
10
+ return func
11
+
12
+ from .simpleImage import ImageInput, SimpleImage
13
+
14
+
15
+ class BinaryProvider:
16
+
17
+ # Constructor
18
+ def __init__(
19
+ self,
20
+ hiddenString,
21
+ startString="START-VALIDATION\n",
22
+ endString="\nEND-VALIDATION",
23
+ ):
24
+ self.hiddenBits = self._stringToBits(hiddenString)
25
+ self.startBits = self._stringToBits(startString)
26
+ self.endBits = self._stringToBits(endString)
27
+
28
+ # Convert string to contiguous binary digits
29
+ def _stringToBits(self, string):
30
+ bits = []
31
+ for char in string:
32
+ binary = format(ord(char), "08b")
33
+ bits.extend(int(bit) for bit in binary)
34
+ return bits
35
+
36
+ def _expandPayload(self, count: int):
37
+ if count <= 0:
38
+ return []
39
+ payloadLen = len(self.hiddenBits)
40
+ if payloadLen == 0:
41
+ raise ValueError("Hidden payload is empty")
42
+ repeats, remainder = divmod(count, payloadLen)
43
+ return (self.hiddenBits * repeats) + self.hiddenBits[:remainder]
44
+
45
+ def buildBitArray(self, pixelCount: int):
46
+ startLen = len(self.startBits)
47
+ endLen = len(self.endBits)
48
+ if pixelCount < startLen + endLen:
49
+ raise ValueError("Image is too small to fit start/end sentinels")
50
+
51
+ bits = [0] * pixelCount
52
+
53
+ # 1. Place START-VALIDATION bits first
54
+ bits[:startLen] = self.startBits
55
+
56
+ # 2. Fill the remaining slots by repeating the payload bits
57
+ payloadSlots = pixelCount - startLen
58
+ payloadBits = self._expandPayload(payloadSlots)
59
+ bits[startLen:] = payloadBits[:payloadSlots]
60
+
61
+ # 3. Overwrite the tail with END-VALIDATION bits
62
+ tailStart = pixelCount - endLen
63
+ bits[tailStart:] = self.endBits
64
+
65
+ return bits
66
+
67
+
68
+ @profile
69
+ def addHiddenBit(imageInput: ImageInput, hiddenBinary: BinaryProvider):
70
+ img = SimpleImage.open(imageInput)
71
+ width, height = img.size
72
+ pixels = img._pixels # direct buffer access for performance
73
+ total = width * height
74
+ payloadBits = hiddenBinary.buildBitArray(total)
75
+
76
+ # Iterate over every pixel and inject one bit
77
+ for idx in range(total):
78
+ base = idx * 3
79
+ # Read the pixel
80
+ r = pixels[base]
81
+ g = pixels[base + 1]
82
+ b = pixels[base + 2]
83
+
84
+ # Calculate the distance from 127
85
+ diffR = r - 127
86
+ if diffR < 0:
87
+ diffR = -diffR
88
+ diffG = g - 127
89
+ if diffG < 0:
90
+ diffG = -diffG
91
+ diffB = b - 127
92
+ if diffB < 0:
93
+ diffB = -diffB
94
+
95
+ # Pick the component farthest from 127
96
+ maxDiff = diffR
97
+ if diffG > maxDiff:
98
+ maxDiff = diffG
99
+ if diffB > maxDiff:
100
+ maxDiff = diffB
101
+
102
+ # Actual value of that channel
103
+ if maxDiff == diffR:
104
+ targetColorValue = r
105
+ elif maxDiff == diffG:
106
+ targetColorValue = g
107
+ else:
108
+ targetColorValue = b
109
+
110
+ # Channels >=127 are decremented, <127 incremented
111
+ addDirection = 1 if targetColorValue < 127 else -1
112
+
113
+ # Pull next bit from provider
114
+ bit = payloadBits[idx]
115
+
116
+ # Force the selected channel parity to match the bit
117
+ if maxDiff == diffR:
118
+ if r % 2 != bit:
119
+ r += addDirection
120
+ if maxDiff == diffG:
121
+ if g % 2 != bit:
122
+ g += addDirection
123
+ if maxDiff == diffB:
124
+ if b % 2 != bit:
125
+ b += addDirection
126
+
127
+ # Write the updated pixel
128
+ pixels[base] = r
129
+ pixels[base + 1] = g
130
+ pixels[base + 2] = b
131
+
132
+ # Return the modified image
133
+ return img
134
+
135
+
136
+ def stringCryptor(plaintext: str, public_key) -> str:
137
+
138
+ ciphertext = public_key.encrypt(
139
+ plaintext.encode("utf-8"),
140
+ padding.OAEP(
141
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
142
+ algorithm=hashes.SHA256(),
143
+ label=None,
144
+ ),
145
+ )
146
+
147
+ return base64.b64encode(ciphertext).decode("ascii")
148
+
149
+
150
+ # main
151
+ # Image input (path or bytes) + payload string => returns image with embedded payload
152
+ def signImage(imageInput: ImageInput, hiddenString, publicKeyPath=None):
153
+ """
154
+ Embed a payload into an image using the parity-based steganography scheme.
155
+
156
+ Args:
157
+ imageInput: File path, bytes, or file-like object accepted by SimpleImage.
158
+ hiddenString: Text payload that should be written into the image.
159
+ publicKeyPath: Optional path to a PEM-encoded RSA public key used to
160
+ encrypt the payload and sentinel markers before embedding.
161
+
162
+ Returns:
163
+ SimpleImage instance whose pixels include the signed payload.
164
+
165
+ Raises:
166
+ FileNotFoundError: If a public key path is provided but the file is missing.
167
+ ValueError: If the file is not a valid PEM public key.
168
+ """
169
+
170
+ if publicKeyPath: # When encryption key is supplied
171
+ key_path = Path(publicKeyPath)
172
+ if not key_path.is_file():
173
+ raise FileNotFoundError(f"Public key file not found: {publicKeyPath}")
174
+
175
+ pem_data = key_path.read_bytes()
176
+ if b"BEGIN PUBLIC KEY" not in pem_data:
177
+ raise ValueError("Provided file does not contain a valid public key")
178
+
179
+ public_key = serialization.load_pem_public_key(pem_data)
180
+
181
+ hiddenBinary = BinaryProvider(
182
+ hiddenString=stringCryptor(hiddenString, public_key) + "\n",
183
+ startString=stringCryptor("START-VALIDATION", public_key) + "\n",
184
+ endString="\n" + stringCryptor("END-VALIDATION", public_key),
185
+ )
186
+
187
+ else: # Plain-text payload
188
+ hiddenBinary = BinaryProvider(hiddenString + "\n")
189
+
190
+ signedImage = addHiddenBit(imageInput, hiddenBinary)
191
+ return signedImage
@@ -0,0 +1,206 @@
1
+ import base64
2
+ from cryptography.hazmat.primitives import serialization, hashes
3
+ from cryptography.hazmat.primitives.asymmetric import padding
4
+
5
+ try:
6
+ from line_profiler import profile # type: ignore
7
+ except ImportError: # pragma: no cover
8
+ def profile(func):
9
+ return func
10
+
11
+ from .simpleImage import ImageInput, SimpleImage
12
+
13
+
14
+ def binaryToString(binaryCode):
15
+ string = []
16
+ for i in range(0, len(binaryCode), 8):
17
+ byte = binaryCode[i : i + 8]
18
+ decimal = int(byte, 2)
19
+ character = chr(decimal)
20
+ string.append(character)
21
+ return "".join(string)
22
+
23
+
24
+ @profile
25
+ def readHiddenBit(imageInput: ImageInput):
26
+ img = SimpleImage.open(imageInput)
27
+ width, height = img.size
28
+ pixels = img._pixels # direct buffer access for performance
29
+ total = width * height
30
+ bits = []
31
+ append_bit = bits.append
32
+
33
+ for idx in range(total):
34
+ base = idx * 3
35
+ r = pixels[base]
36
+ g = pixels[base + 1]
37
+ b = pixels[base + 2]
38
+
39
+ diffR = r - 127
40
+ if diffR < 0:
41
+ diffR = -diffR
42
+ diffG = g - 127
43
+ if diffG < 0:
44
+ diffG = -diffG
45
+ diffB = b - 127
46
+ if diffB < 0:
47
+ diffB = -diffB
48
+
49
+ maxDiff = diffR
50
+ if diffG > maxDiff:
51
+ maxDiff = diffG
52
+ if diffB > maxDiff:
53
+ maxDiff = diffB
54
+
55
+ append_bit("1" if maxDiff % 2 == 0 else "0")
56
+
57
+ return "".join(bits)
58
+
59
+
60
+ def deduplicate(arr):
61
+ deduplicated = []
62
+ freq = {}
63
+ most_common = None
64
+ most_count = 0
65
+
66
+ for i, value in enumerate(arr):
67
+ freq[value] = freq.get(value, 0) + 1
68
+ if freq[value] > most_count:
69
+ most_count = freq[value]
70
+ most_common = value
71
+
72
+ if i == 0 or value != arr[i - 1]:
73
+ deduplicated.append(value)
74
+
75
+ return deduplicated, most_common
76
+
77
+
78
+ def tailCheck(arr: list[str]):
79
+ if len(arr) != 4:
80
+ return None # Not required
81
+
82
+ full_cipher = arr[1] # complete ciphertext
83
+ truncated_cipher = arr[2] # incomplete ciphertext
84
+
85
+ return full_cipher.startswith(truncated_cipher)
86
+
87
+
88
+ def buildValidationReport(decrypted, tailCheck: bool, skipPlain: bool = False):
89
+ # Length after deduplication/decryption
90
+ arrayLength = len(decrypted)
91
+
92
+ # 1. Check that the deduplicated sequence length is valid
93
+ lengthCheck = arrayLength in (3, 4)
94
+
95
+ # 2. Validate start/end markers
96
+ startCheck = decrypted[0] == "START-VALIDATION" if decrypted else False
97
+ endCheck = decrypted[-1] == "END-VALIDATION" if decrypted else False
98
+
99
+ # 4. Determine whether payload was successfully decrypted
100
+ decryptedPayload = decrypted[1] if len(decrypted) > 1 else ""
101
+ isDecrypted = bool(decryptedPayload) and not decryptedPayload.endswith("==")
102
+
103
+ checkList = [lengthCheck, startCheck, endCheck, isDecrypted]
104
+ # 5. Parse tailCheck result
105
+ if tailCheck is None:
106
+ tailCheckResult = "Not Required"
107
+ else:
108
+ tailCheckResult = tailCheck
109
+ checkList.append(tailCheckResult)
110
+
111
+ # Overall verdict requires every check to pass
112
+ verdict = all(checkList)
113
+
114
+ result = {
115
+ "arrayLength": arrayLength,
116
+ "lengthCheck": lengthCheck,
117
+ "startCheck": startCheck,
118
+ "endCheck": endCheck,
119
+ "isDecrypted": isDecrypted,
120
+ "tailCheckResult": tailCheckResult,
121
+ "verdict": verdict,
122
+ }
123
+
124
+ if skipPlain:
125
+ result["decryptSkipMessage"] = (
126
+ "Skip decrypt: payload was plain or corrupted text despite decrypt request."
127
+ )
128
+
129
+ return result
130
+
131
+
132
+ def decrypt_array(deduplicated, privKeyPath):
133
+ # Load PEM private key
134
+ with open(privKeyPath, "rb") as key_file:
135
+ private_key = serialization.load_pem_private_key(
136
+ key_file.read(),
137
+ password=None,
138
+ )
139
+
140
+ decrypted = []
141
+ skippedPlainCount = 0
142
+ decryptError = False
143
+ for item in deduplicated:
144
+ if item.endswith("=="):
145
+ try:
146
+ cipher_bytes = base64.b64decode(item)
147
+ plain_bytes = private_key.decrypt(
148
+ cipher_bytes,
149
+ padding.OAEP(
150
+ mgf=padding.MGF1(algorithm=hashes.SHA256()),
151
+ algorithm=hashes.SHA256(),
152
+ label=None,
153
+ ),
154
+ )
155
+ decrypted.append(plain_bytes.decode("utf-8"))
156
+ except Exception as exc:
157
+ print(exc)
158
+ decryptError = True
159
+ decrypted.append(item)
160
+ else:
161
+ skippedPlainCount += 1
162
+ decrypted.append(item)
163
+
164
+ expectedPlainCount = 0
165
+ if len(deduplicated) == 4:
166
+ expectedPlainCount = 1
167
+
168
+ skippedPlain = decryptError or skippedPlainCount != expectedPlainCount
169
+
170
+ return decrypted, skippedPlain
171
+
172
+
173
+ # main
174
+ def validateImage(imageInput: ImageInput, privKeyPath=None):
175
+ """
176
+ Extract the embedded payload from an image and optionally decrypt it.
177
+
178
+ Args:
179
+ imageInput: File path, bytes, or file-like object accepted by SimpleImage.
180
+ privKeyPath: Optional path to a PEM-encoded RSA private key used to
181
+ decrypt the extracted ciphertext.
182
+
183
+ Returns:
184
+ Dict with the most common extracted string, decrypted sequence, and
185
+ a validation report describing the sentinel checks and verdict.
186
+ """
187
+ resultBinary = readHiddenBit(imageInput)
188
+ resultString = binaryToString(resultBinary)
189
+ splited = resultString.split("\n")
190
+ deduplicated, most_common = deduplicate(splited)
191
+
192
+ if privKeyPath:
193
+ decrypted, skippedPlain = decrypt_array(deduplicated, privKeyPath)
194
+ else:
195
+ decrypted = deduplicated
196
+ skippedPlain = False
197
+
198
+ report = buildValidationReport(
199
+ decrypted=decrypted, tailCheck=tailCheck(deduplicated), skipPlain=skippedPlain
200
+ )
201
+
202
+ return {
203
+ "extractedString": decrypt_array({most_common}, privKeyPath)[0][0],
204
+ "decrypted": decrypted,
205
+ "validationReport": report,
206
+ }
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"]
@@ -0,0 +1,319 @@
1
+ import struct
2
+ import zlib
3
+ from io import BytesIO
4
+ from pathlib import Path
5
+ from typing import List, Sequence, Tuple, Union
6
+
7
+ PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"
8
+
9
+
10
+ def _paethPredictor(a: int, b: int, c: int) -> int:
11
+ p = a + b - c
12
+ pa = abs(p - a)
13
+ pb = abs(p - b)
14
+ pc = abs(p - c)
15
+ if pa <= pb and pa <= pc:
16
+ return a
17
+ if pb <= pc:
18
+ return b
19
+ return c
20
+
21
+
22
+ def _applyPngFilter(
23
+ filterType: int, rowData: bytearray, prevRow: Sequence[int], bytesPerPixel: int
24
+ ) -> bytearray:
25
+ recon = bytearray(len(rowData))
26
+ for i in range(len(rowData)):
27
+ left = recon[i - bytesPerPixel] if i >= bytesPerPixel else 0
28
+ up = prevRow[i] if prevRow else 0
29
+ upLeft = prevRow[i - bytesPerPixel] if (prevRow and i >= bytesPerPixel) else 0
30
+
31
+ if filterType == 0:
32
+ recon[i] = rowData[i]
33
+ elif filterType == 1:
34
+ recon[i] = (rowData[i] + left) & 0xFF
35
+ elif filterType == 2:
36
+ recon[i] = (rowData[i] + up) & 0xFF
37
+ elif filterType == 3:
38
+ recon[i] = (rowData[i] + ((left + up) >> 1)) & 0xFF
39
+ elif filterType == 4:
40
+ recon[i] = (rowData[i] + _paethPredictor(left, up, upLeft)) & 0xFF
41
+ else:
42
+ raise ValueError(f"Unsupported PNG filter: {filterType}")
43
+ return recon
44
+
45
+
46
+ def _readChunk(stream) -> Tuple[bytes, bytes]:
47
+ lengthBytes = stream.read(4)
48
+ if len(lengthBytes) == 0:
49
+ return b"", b""
50
+ if len(lengthBytes) != 4:
51
+ raise ValueError("Unexpected EOF while reading chunk length")
52
+ length = struct.unpack(">I", lengthBytes)[0]
53
+ chunkType = stream.read(4)
54
+ if len(chunkType) != 4:
55
+ raise ValueError("Unexpected EOF while reading chunk type")
56
+ data = stream.read(length)
57
+ if len(data) != length:
58
+ raise ValueError("Unexpected EOF while reading chunk data")
59
+ crc = stream.read(4)
60
+ if len(crc) != 4:
61
+ raise ValueError("Unexpected EOF while reading chunk CRC")
62
+ expectedCrc = zlib.crc32(chunkType)
63
+ expectedCrc = zlib.crc32(data, expectedCrc) & 0xFFFFFFFF
64
+ actualCrc = struct.unpack(">I", crc)[0]
65
+ if actualCrc != expectedCrc:
66
+ raise ValueError("Corrupted PNG chunk detected")
67
+ return chunkType, data
68
+
69
+
70
+ def _loadPng(stream) -> Tuple[int, int, bytearray]:
71
+ signature = stream.read(8)
72
+ if signature != PNG_SIGNATURE:
73
+ raise ValueError("Unsupported PNG signature")
74
+
75
+ width = height = None
76
+ bitDepth = colorType = None
77
+ compression = filterMethod = interlace = None
78
+ idatChunks: List[bytes] = []
79
+
80
+ while True:
81
+ chunkType, data = _readChunk(stream)
82
+ if chunkType == b"":
83
+ break
84
+ if chunkType == b"IHDR":
85
+ (
86
+ width,
87
+ height,
88
+ bitDepth,
89
+ colorType,
90
+ compression,
91
+ filterMethod,
92
+ interlace,
93
+ ) = struct.unpack(">IIBBBBB", data)
94
+ elif chunkType == b"IDAT":
95
+ idatChunks.append(data)
96
+ elif chunkType == b"IEND":
97
+ break
98
+
99
+ if None in (
100
+ width,
101
+ height,
102
+ bitDepth,
103
+ colorType,
104
+ compression,
105
+ filterMethod,
106
+ interlace,
107
+ ):
108
+ raise ValueError("Incomplete PNG header information")
109
+ if bitDepth != 8:
110
+ raise ValueError("Only 8-bit PNG images are supported")
111
+ if colorType not in (2, 6):
112
+ raise ValueError("Only RGB/RGBA PNG images are supported")
113
+ if compression != 0 or filterMethod != 0 or interlace != 0:
114
+ raise ValueError("Unsupported PNG configuration (compression/filter/interlace)")
115
+
116
+ rawImage = zlib.decompress(b"".join(idatChunks))
117
+ bytesPerPixel = 3 if colorType == 2 else 4
118
+ rowLength = width * bytesPerPixel
119
+ expected = height * (rowLength + 1)
120
+ if len(rawImage) != expected:
121
+ raise ValueError("Malformed PNG image data")
122
+
123
+ pixels = bytearray(width * height * 3)
124
+ prevRow = bytearray(rowLength)
125
+ offset = 0
126
+ for y in range(height):
127
+ filterType = rawImage[offset]
128
+ offset += 1
129
+ rowBytes = bytearray(rawImage[offset : offset + rowLength])
130
+ offset += rowLength
131
+ recon = _applyPngFilter(filterType, rowBytes, prevRow, bytesPerPixel)
132
+ for x in range(width):
133
+ srcIndex = x * bytesPerPixel
134
+ destIndex = (y * width + x) * 3
135
+ pixels[destIndex] = recon[srcIndex]
136
+ pixels[destIndex + 1] = recon[srcIndex + 1]
137
+ pixels[destIndex + 2] = recon[srcIndex + 2]
138
+ prevRow = recon
139
+ return width, height, pixels
140
+
141
+
142
+ def _loadBmp(stream) -> Tuple[int, int, bytearray]:
143
+ header = stream.read(14)
144
+ if len(header) != 14 or header[:2] != b"BM":
145
+ raise ValueError("Unsupported BMP header")
146
+ fileSize, _, _, pixelOffset = struct.unpack("<IHHI", header[2:])
147
+ dibHeaderSizeBytes = stream.read(4)
148
+ if len(dibHeaderSizeBytes) != 4:
149
+ raise ValueError("Corrupted BMP DIB header")
150
+ dibHeaderSize = struct.unpack("<I", dibHeaderSizeBytes)[0]
151
+ if dibHeaderSize != 40:
152
+ raise ValueError("Only BITMAPINFOHEADER BMP files are supported")
153
+ dibData = stream.read(36)
154
+ (
155
+ width,
156
+ height,
157
+ planes,
158
+ bitCount,
159
+ compression,
160
+ imageSize,
161
+ xPpm,
162
+ yPpm,
163
+ clrUsed,
164
+ clrImportant,
165
+ ) = struct.unpack("<iiHHIIiiII", dibData)
166
+ if planes != 1 or bitCount != 24 or compression != 0:
167
+ raise ValueError("Only uncompressed 24-bit BMP files are supported")
168
+ absHeight = abs(height)
169
+ rowStride = ((width * 3 + 3) // 4) * 4
170
+ pixels = bytearray(width * absHeight * 3)
171
+ stream.seek(pixelOffset)
172
+ for row in range(absHeight):
173
+ rowData = stream.read(rowStride)
174
+ if len(rowData) != rowStride:
175
+ raise ValueError("Incomplete BMP pixel data")
176
+ targetRow = absHeight - 1 - row if height > 0 else row
177
+ baseIndex = targetRow * width * 3
178
+ for x in range(width):
179
+ pixelOffsetInRow = x * 3
180
+ b = rowData[pixelOffsetInRow]
181
+ g = rowData[pixelOffsetInRow + 1]
182
+ r = rowData[pixelOffsetInRow + 2]
183
+ dest = baseIndex + x * 3
184
+ pixels[dest] = r & 0xFF
185
+ pixels[dest + 1] = g & 0xFF
186
+ pixels[dest + 2] = b & 0xFF
187
+ return width, absHeight, pixels
188
+
189
+
190
+ def _makeChunk(chunkType: bytes, data: bytes) -> bytes:
191
+ length = struct.pack(">I", len(data))
192
+ crcValue = zlib.crc32(chunkType)
193
+ crcValue = zlib.crc32(data, crcValue) & 0xFFFFFFFF
194
+ crc = struct.pack(">I", crcValue)
195
+ return length + chunkType + data + crc
196
+
197
+
198
+ def _writePng(path: str, width: int, height: int, pixels: Sequence[int]) -> None:
199
+ ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
200
+ rowStride = width * 3
201
+ raw = bytearray()
202
+ for y in range(height):
203
+ raw.append(0)
204
+ start = y * rowStride
205
+ raw.extend(pixels[start : start + rowStride])
206
+ compressed = zlib.compress(bytes(raw))
207
+ with open(path, "wb") as output:
208
+ output.write(PNG_SIGNATURE)
209
+ output.write(_makeChunk(b"IHDR", ihdr))
210
+ output.write(_makeChunk(b"IDAT", compressed))
211
+ output.write(_makeChunk(b"IEND", b""))
212
+
213
+
214
+ def _writeBmp(path: str, width: int, height: int, pixels: Sequence[int]) -> None:
215
+ rowStride = ((width * 3 + 3) // 4) * 4
216
+ pixelArraySize = rowStride * height
217
+ fileSize = 14 + 40 + pixelArraySize
218
+ with open(path, "wb") as output:
219
+ output.write(b"BM")
220
+ output.write(struct.pack("<IHHI", fileSize, 0, 0, 54))
221
+ output.write(
222
+ struct.pack(
223
+ "<IIIHHIIIIII",
224
+ 40,
225
+ width,
226
+ height,
227
+ 1,
228
+ 24,
229
+ 0,
230
+ pixelArraySize,
231
+ 2835,
232
+ 2835,
233
+ 0,
234
+ 0,
235
+ )
236
+ )
237
+ rowPad = rowStride - width * 3
238
+ padBytes = b"\x00" * rowPad
239
+ for y in range(height - 1, -1, -1):
240
+ start = y * width * 3
241
+ for x in range(width):
242
+ idx = start + x * 3
243
+ r = pixels[idx]
244
+ g = pixels[idx + 1]
245
+ b = pixels[idx + 2]
246
+ output.write(bytes((b & 0xFF, g & 0xFF, r & 0xFF)))
247
+ if rowPad:
248
+ output.write(padBytes)
249
+
250
+
251
+ ImageInput = Union[str, Path, bytes, bytearray]
252
+
253
+
254
+ class SimpleImage:
255
+ __slots__ = ("width", "height", "_pixels")
256
+
257
+ def __init__(self, width: int, height: int, pixels: Sequence[int]):
258
+ self.width = width
259
+ self.height = height
260
+ expected = width * height * 3
261
+ if len(pixels) != expected:
262
+ raise ValueError("Pixel data length does not match image dimensions")
263
+ self._pixels = bytearray(pixels)
264
+
265
+ @property
266
+ def size(self) -> Tuple[int, int]:
267
+ return self.width, self.height
268
+
269
+ @staticmethod
270
+ def _streamToImage(stream) -> Tuple[int, int, bytearray]:
271
+ signature = stream.read(8)
272
+ stream.seek(0)
273
+ if signature.startswith(PNG_SIGNATURE):
274
+ return _loadPng(stream)
275
+ if signature[:2] == b"BM":
276
+ return _loadBmp(stream)
277
+ raise ValueError("Unsupported image format")
278
+
279
+ @classmethod
280
+ def open(cls, source: ImageInput) -> "SimpleImage":
281
+ if isinstance(source, (str, Path)):
282
+ with open(source, "rb") as stream:
283
+ width, height, pixels = cls._streamToImage(stream)
284
+ elif isinstance(source, (bytes, bytearray)):
285
+ stream = BytesIO(source)
286
+ width, height, pixels = cls._streamToImage(stream)
287
+ else:
288
+ raise TypeError("source must be a file path or raw bytes")
289
+ return cls(width, height, pixels)
290
+
291
+ def getPixel(self, coords: Tuple[int, int]) -> Tuple[int, int, int]:
292
+ x, y = coords
293
+ if not (0 <= x < self.width and 0 <= y < self.height):
294
+ raise ValueError("Pixel coordinate out of bounds")
295
+ index = (y * self.width + x) * 3
296
+ return (
297
+ self._pixels[index],
298
+ self._pixels[index + 1],
299
+ self._pixels[index + 2],
300
+ )
301
+
302
+ def putPixel(self, coords: Tuple[int, int], value: Sequence[int]) -> None:
303
+ x, y = coords
304
+ if not (0 <= x < self.width and 0 <= y < self.height):
305
+ raise ValueError("Pixel coordinate out of bounds")
306
+ index = (y * self.width + x) * 3
307
+ r, g, b = value
308
+ self._pixels[index] = int(r) & 0xFF
309
+ self._pixels[index + 1] = int(g) & 0xFF
310
+ self._pixels[index + 2] = int(b) & 0xFF
311
+
312
+ def copy(self) -> "SimpleImage":
313
+ return SimpleImage(self.width, self.height, self._pixels[:])
314
+
315
+ def save(self, path: str) -> None:
316
+ _writePng(path, self.width, self.height, self._pixels)
317
+
318
+ def saveBmp(self, path: str) -> None:
319
+ _writeBmp(path, self.width, self.height, self._pixels)
@@ -0,0 +1,7 @@
1
+ Metadata-Version: 2.4
2
+ Name: Pixseal
3
+ Version: 0.2.1.post1
4
+ Summary: Encrypted Image Watermark Injector / Validator
5
+ Requires-Python: >=3.8
6
+ Description-Content-Type: text/markdown
7
+ Requires-Dist: cryptography>=41.0.0
@@ -0,0 +1,10 @@
1
+ Pixseal/__init__.py,sha256=8J9y9waCvvaLBhcK7s6Z_q7yuIKORbuxvt0UPkikJr8,488
2
+ Pixseal/imageSigner.py,sha256=WrHHKCmtdf_qutAd9P5TVaraQEaLkHcSGMl5Rfoq6ng,5979
3
+ Pixseal/imageValidator.py,sha256=YnM5UNRFMqDjUGOVAozBM0LsC7IkqIpXIxEMLLtnIWM,5949
4
+ Pixseal/simpleImage.py,sha256=dkpMEhkT1z0pZOgy-AbgYlF9GC73eM2WGaYb5FiGNpk,1234
5
+ Pixseal/simpleImage_ext.cpython-314-x86_64-linux-gnu.so,sha256=-ugmkXblsIVni84ORQeqe34qf4ATZ74-54yFVQRWxDU,2695488
6
+ Pixseal/simpleImage_py.py,sha256=jJajVT8Y7Cv_hZ_irmCWtVqQwcg66VEb2lhF9u-SYK8,10849
7
+ pixseal-0.2.1.post1.dist-info/METADATA,sha256=pUkqjUSucm1F9AXwyzsL0h6KHjgLsEdkiWHk_ybEWC4,212
8
+ pixseal-0.2.1.post1.dist-info/WHEEL,sha256=DMni2HWXXG4oat8Y6bAVuu-xlkB-grHQiqsTu-lLHls,190
9
+ pixseal-0.2.1.post1.dist-info/top_level.txt,sha256=q35ICL7vJyo5hOpL4YbL5t_g4297gwUPIuLy82kgL3s,8
10
+ pixseal-0.2.1.post1.dist-info/RECORD,,
@@ -0,0 +1,7 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (80.9.0)
3
+ Root-Is-Purelib: false
4
+ Tag: cp314-cp314-manylinux_2_17_x86_64
5
+ Tag: cp314-cp314-manylinux2014_x86_64
6
+ Tag: cp314-cp314-manylinux_2_28_x86_64
7
+
@@ -0,0 +1 @@
1
+ Pixseal