import argparse import os import zipfile import tempfile import shutil import numpy as np from PIL import Image, ImageChops import xml.etree.ElementTree as ET NUM_LAYERS = 4 MASKS_PER_LAYER = 3 NOISE_SCALES = [4, 16, 64] def noise_mask(w, h, scale): sw = max(1, w // scale) sh = max(1, h // scale) noise = np.random.rand(sh, sw) img = Image.fromarray((noise * 255).astype(np.uint8), "L") img = img.resize((w, h), Image.BILINEAR) return img def build_stack_xml(w, h, groups): image = ET.Element( "image", {"version": "0.0.3", "w": str(w), "h": str(h)} ) root_stack = ET.SubElement(image, "stack") for group in groups: group_el = ET.SubElement( root_stack, "stack", {"name": group["name"]} ) for layer in group["layers"]: layer_attrs = { "name": layer["name"], "src": layer["src"], "opacity": "1.0" } ET.SubElement( group_el, "layer", layer_attrs ) return ET.tostring(image, encoding="utf-8", xml_declaration=True) def build_ora(input_png, output_ora): base = Image.open(input_png).convert("RGBA") w, h = base.size tmp = tempfile.mkdtemp() try: data_dir = os.path.join(tmp, "data") thumb_dir = os.path.join(tmp, "Thumbnails") os.makedirs(data_dir) os.makedirs(thumb_dir) groups = [] for i in range(NUM_LAYERS): group_layers = [] for j in range(MASKS_PER_LAYER): mask = noise_mask(w, h, NOISE_SCALES[j]) # Apply mask to alpha channel of base image masked_image = base.copy() r, g, b, a = masked_image.split() # Multiply existing alpha by mask new_alpha = ImageChops.multiply(a, mask) masked_image.putalpha(new_alpha) layer_path = f"data/layer_{i}_{j}.png" masked_image.save(os.path.join(tmp, layer_path)) group_layers.append({ "name": f"layer {j}", "src": layer_path }) groups.append({ "name": f"group {i}", "layers": group_layers }) stack_xml = build_stack_xml(w, h, groups) with open(os.path.join(tmp, "stack.xml"), "wb") as f: f.write(stack_xml) with open(os.path.join(tmp, "mimetype"), "w") as f: f.write("image/openraster") base.save(os.path.join(tmp, "mergedimage.png")) thumb = base.copy() thumb.thumbnail((256, 256)) thumb.save(os.path.join(thumb_dir, "thumbnail.png")) with zipfile.ZipFile(output_ora, "w") as z: z.write( os.path.join(tmp, "mimetype"), "mimetype", compress_type=zipfile.ZIP_STORED ) for root, _, files in os.walk(tmp): for file in files: if file == "mimetype": continue full = os.path.join(root, file) rel = os.path.relpath(full, tmp) z.write(full, rel) finally: shutil.rmtree(tmp) def main(): parser = argparse.ArgumentParser( description="Generate ORA with grouped layers and mask layers" ) parser.add_argument("input_png") parser.add_argument("output_ora") args = parser.parse_args() build_ora(args.input_png, args.output_ora) if __name__ == "__main__": main()