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)