chromacode / app.py
HedronCreeper's picture
Update app.py
3230f87 verified
from flask import Flask, request, jsonify, send_from_directory
from PIL import Image, ImageDraw, ImageFont
import io, base64, struct, math
app = Flask(__name__, static_folder="static")
BG_COLOR = (88, 28, 135)
TEXT_COLOR = (255, 255, 255)
BITS_PER_CHANNEL = 2
MASK = (1 << BITS_PER_CHANNEL) - 1
MAX_BYTES = 5 * 1024 * 1024
MIN_SIZE = 200
def get_font_for_height(target_px):
font_path = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
size = max(1, target_px)
for _ in range(50):
try:
font = ImageFont.truetype(font_path, size)
except:
return ImageFont.load_default()
dummy = Image.new("RGB", (1, 1))
draw = ImageDraw.Draw(dummy)
bbox = draw.textbbox((0, 0), "CC", font=font)
actual_h = bbox[3] - bbox[1]
if abs(actual_h - target_px) <= 1:
break
if actual_h == 0:
size += 1
else:
size = max(1, int(size * target_px / actual_h))
return font
def make_base_image(w, h):
img = Image.new("RGB", (w, h), BG_COLOR)
draw = ImageDraw.Draw(img)
# Make the text 5 times bigger
target_h = int(min(w, h) * 10.0) # Increased from 2.0 to 10.0 (5x larger)
target_h = min(target_h, min(w, h) - 4)
font = get_font_for_height(target_h)
bbox = draw.textbbox((0, 0), "CC", font=font)
tw = bbox[2] - bbox[0]
th = bbox[3] - bbox[1]
x = (w - tw) // 2 - bbox[0]
y = (h - th) // 2 - bbox[1]
draw.text((x, y), "CC", fill=TEXT_COLOR, font=font)
return img
def calc_size(payload_bytes):
total_bits = (len(payload_bytes) + 4) * 8
pixels_needed = math.ceil(total_bits / 6)
# Increase size calculation to accommodate larger text
side = max(MIN_SIZE, math.ceil(math.sqrt(pixels_needed * 4.5 * 25))) # 25 = 5^2 (area scaling factor)
return side, side
def get_carriers(w, h):
base = make_base_image(w, h)
pixels = base.load()
carriers = []
for y in range(h):
for x in range(w):
if pixels[x, y] == BG_COLOR:
carriers.append((x, y))
return carriers, base
def bits_from_bytes(data):
bits = []
for byte in data:
for i in range(7, -1, -1):
bits.append((byte >> i) & 1)
return bits
def bytes_from_bits(bits):
result = []
for i in range(0, len(bits), 8):
chunk = bits[i:i+8]
if len(chunk) < 8:
break
byte = 0
for b in chunk:
byte = (byte << 1) | b
result.append(byte)
return bytes(result)
def embed(payload):
w, h = calc_size(payload)
for _ in range(10):
carriers, img = get_carriers(w, h)
header = struct.pack(">I", len(payload))
data = header + payload
bits = bits_from_bytes(data)
if len(bits) <= len(carriers) * BITS_PER_CHANNEL * 3:
break
w = math.ceil(w * 1.2)
h = math.ceil(h * 1.2)
else:
raise ValueError("Data too large even after resizing.")
pixels = img.load()
bit_idx = 0
for (x, y) in carriers:
if bit_idx >= len(bits):
break
r, g, b = pixels[x, y]
new_channels = []
for c in [r, g, b]:
chunk = 0
for _ in range(BITS_PER_CHANNEL):
if bit_idx < len(bits):
chunk = (chunk << 1) | bits[bit_idx]
bit_idx += 1
else:
chunk = chunk << 1
new_channels.append((c & ~MASK) | chunk)
pixels[x, y] = tuple(new_channels)
return img
def extract(img):
w, h = img.size
carriers, _ = get_carriers(w, h)
pixels = img.load()
bits = []
for (x, y) in carriers:
r, g, b = pixels[x, y]
for c in [r, g, b]:
for i in range(BITS_PER_CHANNEL - 1, -1, -1):
bits.append((c >> i) & 1)
length = struct.unpack(">I", bytes_from_bits(bits[:32]))[0]
if length == 0 or length > MAX_BYTES + 10:
return None, None
payload = bytes_from_bits(bits[32:32 + length * 8])
return length, payload
@app.route("/")
def index():
return send_from_directory("static", "index.html")
@app.route("/encode/text", methods=["POST"])
def encode_text():
data = request.json
text = data.get("text", "")
if not text:
return jsonify({"error": "No text provided"}), 400
payload = b'\x00' + text.encode("utf-8")
if len(payload) > MAX_BYTES:
return jsonify({"error": "Text too large. Max 5MB."}), 400
try:
img = embed(payload)
buf = io.BytesIO()
img.save(buf, format="PNG")
img_b64 = base64.b64encode(buf.getvalue()).decode()
w, h = img.size
return jsonify({"image": img_b64, "size": f"{w}×{h}"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/encode/file", methods=["POST"])
def encode_file():
if "file" not in request.files:
return jsonify({"error": "No file uploaded"}), 400
f = request.files["file"]
raw = f.read()
if len(raw) > MAX_BYTES:
return jsonify({"error": f"File too large: {len(raw)/1024/1024:.2f}MB. Max is 5MB."}), 400
fname = f.filename or ""
mode = b'\x02' if fname.lower().endswith(".pdf") else b'\x01'
payload = mode + raw
try:
img = embed(payload)
buf = io.BytesIO()
img.save(buf, format="PNG")
img_b64 = base64.b64encode(buf.getvalue()).decode()
w, h = img.size
return jsonify({"image": img_b64, "size": f"{w}×{h}"})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/decode", methods=["POST"])
def decode_route():
if "file" not in request.files:
return jsonify({"error": "No file uploaded"}), 400
f = request.files["file"]
img = Image.open(f).convert("RGB")
length, payload = extract(img)
if payload is None:
return jsonify({"error": "Nothing encoded or invalid image"}), 400
mode = payload[0]
content = payload[1:]
if mode == 0:
return jsonify({"type": "text", "text": content.decode("utf-8", errors="replace")})
elif mode == 1:
return jsonify({"type": "image", "image": base64.b64encode(content).decode()})
elif mode == 2:
return jsonify({"type": "pdf", "pdf": base64.b64encode(content).decode()})
return jsonify({"error": "Unknown mode"}), 400
if __name__ == "__main__":
app.run(host="0.0.0.0", port=7860)