broadfield-dev commited on
Commit
d238bc6
·
verified ·
1 Parent(s): 38b39e1

Create image_kb_logic.py

Browse files
Files changed (1) hide show
  1. image_kb_logic.py +234 -0
image_kb_logic.py ADDED
@@ -0,0 +1,234 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import io
3
+ import struct
4
+ import logging
5
+ import random
6
+ import json
7
+ from datetime import datetime
8
+ import numpy as np
9
+ from PIL import Image, ImageDraw, ImageFont
10
+ from cryptography.hazmat.primitives.ciphers.aead import AESGCM
11
+ from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
12
+ from cryptography.hazmat.primitives import hashes
13
+ from cryptography.exceptions import InvalidTag
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ KEY_SIZE = 32
18
+ SALT_SIZE = 16
19
+ NONCE_SIZE = 12
20
+ TAG_SIZE = 16
21
+ PBKDF2_ITERATIONS = 480000
22
+ LENGTH_HEADER_SIZE = 4
23
+ PREFERRED_FONTS = ["Arial", "Helvetica", "DejaVu Sans", "Verdana", "Calibri", "sans-serif"]
24
+ MAX_KEYS_TO_DISPLAY_OVERLAY = 15
25
+
26
+ def convert_pil_to_png_bytes(image: Image.Image) -> bytes:
27
+ with io.BytesIO() as buffer:
28
+ image.save(buffer, format="PNG")
29
+ return buffer.getvalue()
30
+
31
+ def _get_font(preferred_fonts, base_size):
32
+ fp = None
33
+ safe_base_size = int(base_size)
34
+ if safe_base_size <= 0: safe_base_size = 10
35
+ for n in preferred_fonts:
36
+ try: ImageFont.truetype(n.lower()+".ttf",10); fp=n.lower()+".ttf"; break
37
+ except IOError:
38
+ try: ImageFont.truetype(n,10); fp=n; break
39
+ except IOError: continue
40
+ if fp:
41
+ try: return ImageFont.truetype(fp, safe_base_size)
42
+ except IOError: logger.warning(f"Font '{fp}' load failed with size {safe_base_size}. Defaulting.")
43
+ try: return ImageFont.load_default(size=safe_base_size)
44
+ except TypeError: return ImageFont.load_default()
45
+
46
+ def set_pil_image_format_to_png(image:Image.Image)->Image.Image:
47
+ buf=io.BytesIO(); image.save(buf,format='PNG'); buf.seek(0)
48
+ reloaded=Image.open(buf); reloaded.format="PNG"; return reloaded
49
+
50
+ def _derive_key(pw:str,salt:bytes)->bytes:
51
+ kdf=PBKDF2HMAC(algorithm=hashes.SHA256(),length=KEY_SIZE,salt=salt,iterations=PBKDF2_ITERATIONS)
52
+ return kdf.derive(pw.encode('utf-8'))
53
+
54
+ def encrypt_data(data:bytes,pw:str)->bytes:
55
+ s=os.urandom(SALT_SIZE);k=_derive_key(pw,s);a=AESGCM(k);n=os.urandom(NONCE_SIZE)
56
+ ct=a.encrypt(n,data,None); return s+n+ct
57
+
58
+ def decrypt_data(payload:bytes,pw:str)->bytes:
59
+ ml=SALT_SIZE+NONCE_SIZE+TAG_SIZE;
60
+ if len(payload)<ml: raise ValueError("Payload too short.")
61
+ s,n,ct_tag=payload[:SALT_SIZE],payload[SALT_SIZE:SALT_SIZE+NONCE_SIZE],payload[SALT_SIZE+NONCE_SIZE:]
62
+ k=_derive_key(pw,s);a=AESGCM(k)
63
+ try: return a.decrypt(n,ct_tag,None)
64
+ except InvalidTag: raise ValueError("Decryption failed: Invalid password/corrupted data.")
65
+ except Exception as e: logger.error(f"Decrypt error: {e}",exc_info=True); raise
66
+
67
+ def _d2b(d:bytes)->str: return ''.join(format(b,'08b') for b in d)
68
+ def _b2B(b:str)->bytes:
69
+ if len(b)%8!=0: raise ValueError("Bits not multiple of 8.")
70
+ return bytes(int(b[i:i+8],2) for i in range(0,len(b),8))
71
+
72
+ def embed_data_in_image(img_obj:Image.Image,data:bytes)->Image.Image:
73
+ img=img_obj.convert("RGB");px=np.array(img);fpx=px.ravel()
74
+ lb=struct.pack('>I',len(data));fp=lb+data;db=_d2b(fp);nb=len(db)
75
+ if nb>len(fpx): raise ValueError(f"Data too large: {nb} bits needed, {len(fpx)} available.")
76
+ for i in range(nb): fpx[i]=(fpx[i]&0xFE)|int(db[i])
77
+ spx=fpx.reshape(px.shape); return Image.fromarray(spx.astype(np.uint8),'RGB')
78
+
79
+ def extract_data_from_image(img_obj:Image.Image)->bytes:
80
+ img=img_obj.convert("RGB");px=np.array(img);fpx=px.ravel()
81
+ hbc=LENGTH_HEADER_SIZE*8
82
+ if len(fpx)<hbc: raise ValueError("Image too small for header.")
83
+ lb="".join(str(fpx[i]&1) for i in range(hbc))
84
+ try: pl=struct.unpack('>I',_b2B(lb))[0]
85
+ except Exception as e: raise ValueError(f"Header decode error: {e}")
86
+ if pl==0: return b""
87
+ if pl>(len(fpx)-hbc)/8: raise ValueError("Header len corrupted or > capacity.")
88
+ tpb=pl*8; so=hbc; eo=so+tpb
89
+ if len(fpx)<eo: raise ValueError("Image truncated or header corrupted.")
90
+ pb="".join(str(fpx[i]&1) for i in range(so,eo)); return _b2B(pb)
91
+
92
+ def parse_kv_string_to_dict(kv_str:str)->dict:
93
+ if not kv_str or not kv_str.strip(): return {}
94
+ dd={};
95
+ for ln,ol in enumerate(kv_str.splitlines(),1):
96
+ l=ol.strip()
97
+ if not l or l.startswith('#'): continue
98
+ lc=l.split('#',1)[0].strip();
99
+ if not lc: continue
100
+ p=lc.split('=',1) if '=' in lc else lc.split(':',1) if ':' in lc else []
101
+ if len(p)!=2: raise ValueError(f"L{ln}: Invalid format '{ol}'.")
102
+ k,v=p[0].strip(),p[1].strip()
103
+ if not k: raise ValueError(f"L{ln}: Empty key in '{ol}'.")
104
+ dd[k]=v
105
+ return dd
106
+
107
+ def convert_kb_to_kv_string(rules: list[str], memories: list[dict], include_rules: bool, include_memories: bool) -> str:
108
+ lines = ["# iLearn Knowledge Base Export", f"# Exported on: {datetime.utcnow().isoformat()}Z"]
109
+
110
+ if include_rules:
111
+ lines.append("\n# --- RULES ---")
112
+ for i, rule_text in enumerate(rules):
113
+ lines.append(f"rule_{i+1} = {json.dumps(rule_text)}")
114
+
115
+ if include_memories:
116
+ lines.append("\n# --- MEMORIES ---")
117
+ for i, mem_dict in enumerate(memories):
118
+ lines.append(f"memory_{i+1} = {json.dumps(mem_dict)}")
119
+
120
+ return "\n".join(lines)
121
+
122
+
123
+ def generate_brain_carrier_image(w=800, h=800) -> Image.Image:
124
+ center_x, center_y = w / 2, h / 2
125
+ y_coords, x_coords = np.mgrid[0:h, 0:w]
126
+
127
+ distance = np.sqrt((x_coords - center_x)**2 + (y_coords - center_y)**2)
128
+ max_distance = np.sqrt(center_x**2 + center_y**2)
129
+
130
+ distance_norm = distance / max_distance
131
+
132
+ bg_center_color = np.array([20, 25, 40])
133
+ bg_outer_color = np.array([0, 0, 0])
134
+
135
+ gradient = bg_outer_color + (bg_center_color - bg_outer_color) * (1 - distance_norm[..., np.newaxis])
136
+
137
+ img = Image.fromarray(gradient.astype(np.uint8), 'RGB')
138
+ draw = ImageDraw.Draw(img)
139
+
140
+ num_distant_stars = int((w * h) / 200)
141
+ for _ in range(num_distant_stars):
142
+ x, y = random.randint(0, w - 1), random.randint(0, h - 1)
143
+ brightness = random.randint(30, 90)
144
+ draw.point((x, y), fill=(brightness, brightness, int(brightness * 1.1)))
145
+
146
+ num_main_stars = int((w * h) / 1000)
147
+ star_colors = [
148
+ (255, 255, 255),
149
+ (220, 230, 255),
150
+ (255, 240, 220),
151
+ ]
152
+
153
+ for _ in range(num_main_stars):
154
+ x, y = random.randint(0, w - 1), random.randint(0, h - 1)
155
+ dist_from_center = np.sqrt((x - center_x)**2 + (y - center_y)**2)
156
+ dist_ratio = min(dist_from_center / max_distance, 1.0)
157
+
158
+ size = 0.5 + (2.5 * (dist_ratio ** 2))
159
+ brightness = 120 + (135 * (dist_ratio ** 1.5))
160
+
161
+ color = random.choice(star_colors)
162
+
163
+ final_color = tuple(int(c * (brightness / 255.0)) for c in color)
164
+
165
+ glow_size = size * 3
166
+ glow_color = tuple(int(c * 0.3) for c in final_color)
167
+ draw.ellipse([x - glow_size, y - glow_size, x + glow_size, y + glow_size], fill=glow_color)
168
+
169
+ if random.random() < 0.15:
170
+ draw.line([x-size, y, x+size, y], fill=final_color, width=1)
171
+ draw.line([x, y-size, x, y+size], fill=final_color, width=1)
172
+ else:
173
+ draw.ellipse([x - size, y - size, x + size, y + size], fill=final_color)
174
+
175
+ return img
176
+
177
+
178
+ def _get_text_measurement(draw_obj, text_str, font_obj):
179
+ if hasattr(draw_obj, 'textbbox'):
180
+ try:
181
+ bbox = draw_obj.textbbox((0, 0), text_str, font=font_obj)
182
+ width = bbox[2] - bbox[0]
183
+ height = bbox[3] - bbox[1]
184
+ return width, height
185
+ except Exception: pass
186
+ try:
187
+ if hasattr(font_obj, 'getsize'): return font_obj.getsize(text_str)
188
+ width, height = draw_obj.textsize(text_str, font=font_obj)
189
+ return width, height
190
+ except AttributeError:
191
+ try:
192
+ char_width_approx = font_obj.size * 0.6
193
+ char_height_approx = font_obj.size
194
+ return int(len(text_str) * char_width_approx), int(char_height_approx)
195
+ except: return len(text_str) * 8, 10
196
+
197
+ def draw_key_list_dropdown_overlay(image: Image.Image, keys: list[str] = None, title: str = "Data Embedded") -> Image.Image:
198
+ img_overlayed = image.copy().convert("RGBA")
199
+ draw = ImageDraw.Draw(img_overlayed, "RGBA")
200
+ width, height = img_overlayed.size
201
+
202
+ overlay_color = (15, 23, 42, 190)
203
+ title_color = (226, 232, 240)
204
+ key_color = (148, 163, 184)
205
+
206
+ font_bold = _get_font(PREFERRED_FONTS, 30)
207
+ font_regular = _get_font(PREFERRED_FONTS, 15)
208
+
209
+ draw.rectangle([0, 20, width, 80], fill=overlay_color)
210
+ draw.text((width / 2, 50), title, fill=title_color, font=font_bold, anchor="ms")
211
+
212
+ if keys:
213
+ box_padding = 15
214
+ line_spacing = 6
215
+ text_start_x = 35
216
+ lines = keys
217
+
218
+ line_heights = [_get_text_measurement(draw, line, font_regular)[1] for line in lines]
219
+ total_text_height = sum(line_heights) + (len(lines) - 1) * line_spacing
220
+ box_height = total_text_height + (box_padding * 2)
221
+ box_y0 = height - box_height - 20
222
+
223
+ draw.rectangle([20, box_y0, width - 20, height - 20], fill=overlay_color)
224
+ current_y = box_y0 + box_padding
225
+
226
+ for i, key_text in enumerate(lines):
227
+ draw.text((text_start_x, current_y), key_text, fill=key_color, font=font_regular)
228
+ if i < len(line_heights):
229
+ current_y += line_heights[i] + line_spacing
230
+
231
+ final_image_rgb = Image.new("RGB", img_overlayed.size, (0, 0, 0))
232
+ final_image_rgb.paste(img_overlayed, (0, 0), img_overlayed)
233
+
234
+ return final_image_rgb