import itertools import io from fastapi.responses import StreamingResponse from pathlib import Path import pyvips import logging from perlin import CoordsGenerator import svg import numpy as np import uvicorn from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware app = FastAPI(title="Speckles API", root_path="/images") origins = [ "http://localhost", "http://localhost:3000", "https://localhost", "https://0124816.xyz", "http://0124816.xyz:3001", ] app.add_middleware( CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) RGen = np.random.default_rng() MEDIA_TYPES = { "png": "image/png", "jpeg": "image/jpeg", "jpg": "image/jpeg", "svg": "image/svg+xml", "pdf": "application/pdf", } def emoji(marker: str, source="twemoji"): if len(marker) != 1: return if source == "openmoji": scaler = 512 file = hex(ord(marker))[2:].upper() elif source == "twemoji": scaler = 768 file = hex(ord(marker))[2:] file = Path(f"{source}/{file}.svg") if file.is_file(): return svg.G( elements=[Include(text=file.read_text())], id=marker, transform=[svg.Scale(100 / scaler)], ) class Include(svg.Element): element_name = "svg" transform: list[svg.Transform] | None = None def __str__(self) -> str: return self.text DEFINED = ".o+tYP-|sSex*" def markertoSVG(marker, x, y, c, s): if marker == ".": return svg.Circle(cx=x, cy=y, fill=c, r=np.sqrt(s)) elif marker == "o": return svg.Circle( cx=x, cy=y, stroke=c, fill="none", stroke_width=np.sqrt(s / 4), r=np.sqrt(s), ) elif marker == "+": shift = np.sqrt(s) return svg.Path( stroke=c, stroke_width=np.sqrt(s) / 4, stroke_linecap="round", d=[ svg.M(x - shift, y), # horizontal line svg.L(x + shift, y), svg.M(x, y - shift), # vertical line svg.L(x, y + shift), ], ) elif marker == "t": shift = np.sqrt(s) return svg.Polygon( stroke=c, stroke_width=np.sqrt(s) / 4, stroke_linejoin="round", points=[ x + shift * np.sqrt(3) / 2, y - shift / 2, x - shift * np.sqrt(3) / 2, y - shift / 2, x, y + shift, ], transform=[svg.Rotate(RGen.random() * 360, x, y)], ) elif marker == "Y": shift = np.sqrt(s) return svg.Path( stroke=c, stroke_width=np.sqrt(s) / 4, stroke_linecap="round", d=[ svg.M(x + shift * np.sqrt(3) / 2, y - shift / 2), # / line svg.L(x, y), svg.M(x - shift * np.sqrt(3) / 2, y - shift / 2), # / line svg.L(x, y), svg.M(x, y + shift), # / line svg.L(x, y), ], transform=[svg.Rotate(RGen.random() * 360, x, y)], ) elif marker == "P": shift = np.sqrt(s) return svg.Path( stroke=c, stroke_width=np.sqrt(s) * 2 / 3, d=[ svg.M(x - shift, y), # horizontal line svg.L(x + shift, y), svg.M(x, y - shift), # vertical line svg.L(x, y + shift), ], ) elif marker == "-": shift = np.sqrt(s) return svg.Path( stroke=c, stroke_width=np.sqrt(s) / 4, d=[ svg.M(x - shift, y), # horizontal line svg.L(x + shift, y), ], ) elif marker == "|": shift = np.sqrt(s) return svg.Path( stroke=c, stroke_width=np.sqrt(s) / 4, d=[ svg.M(x, y - shift), # vertical line svg.L(x, y + shift), ], ) elif marker == "s": shift = np.sqrt(s) * 2 return svg.Rect(x=x, y=y, width=shift, height=shift, fill=c) elif marker == "S": s = np.sqrt(s) * 2 return svg.Rect(x=x, y=y, width=s, height=s, fill=c, rx=s / 3, ry=s / 3) elif marker == "e": s = np.sqrt(s * 2) return svg.Ellipse( cx=x, cy=y, fill=c, rx=s, ry=s / 2, transform=[svg.Rotate(RGen.random() * 360, x, y)], ) elif marker == "x": shift = np.sqrt(s) scale = np.sqrt(2) / 2 return svg.Path( stroke=c, stroke_width=np.sqrt(s) / 4, stroke_linecap="round", d=[ svg.M(x - shift * scale, y - shift * scale), # / line svg.L(x + shift * scale, y + shift * scale), svg.M(x - shift * scale, y + shift * scale), svg.L(x + shift * scale, y - shift * scale), # / line ], ) elif marker == "*": shift = np.sqrt(s) return [ svg.Path( stroke=c, stroke_width=np.sqrt(s) / 4, stroke_linecap="round", transform=[svg.Rotate(angle, x, y)], d=[ svg.M(x - shift, y), svg.L(x + shift, y), ], ) for angle in (0, 60, -60) ] else: return svg.Use( href=f"#{marker}", transform=[svg.Translate(x=x, y=y), svg.Scale(np.sqrt(s) / 100)], ) @app.get("/speckles/") def make_wallpaper( speckle_colours: str, density: float = 0.12, size: float = 3, fileformat: str = "svg", orientation: str = "landscape", filename: str = "", markers: str = ".", perlin: bool = True, ): speckle_colours = speckle_colours.split(",") background = speckle_colours.pop(0) if orientation == "portrait": x, y = (1080, 1920) elif orientation == "landscape": x, y = (1920, 1080) elif "x" in orientation: resolution = orientation.split("x") if len(resolution) != 2: logging.critical("input resolution has more or less than 2 dimensions") return x, y = resolution if all([x.isdigit(), y.isdigit()]): x, y = int(x), int(y) else: return else: x, y = (1920, 1080) speckles_per_colour = int(x / 128 * y / 128 * density / len(markers)) if perlin: gen = CoordsGenerator(x, y) elements = [svg.Rect(width=x, height=y, fill=background)] style = svg.Style( text="\n".join( [f".c{colour[1:]} {{ fill: {colour}; }}" for colour in speckle_colours] ) ) elements.append(style) style = svg.Style( text="\n".join( [ f'.s{hex(int(size*100))[2:]} {{ font-family: "OpenMoji"; font-size: {int(np.sqrt(size)*16)}px; }}' for size in np.logspace(0, size, 10, base=np.exp(2)) ] ) ) elements.append(style) elements.append( svg.Defs( elements=[emoji(marker) for marker in markers if marker not in DEFINED] ) ) for colour, marker, size in itertools.product( speckle_colours, markers, np.logspace(0, size, 10, base=np.exp(2)), ): if perlin: xs, ys = gen.pick(speckles_per_colour) else: xs, ys = zip( RGen.random(speckles_per_colour) * x, RGen.random(speckles_per_colour) * y, ) elements.extend( [markertoSVG(marker, x, y, colour, size) for x, y in zip(xs, ys)] ) content = svg.SVG(width=x, height=y, elements=elements) if filename: if Path(filename).suffix == ".svg": with open(filename, "wt") as f: f.write(content.as_str()) else: with pyvips.Image.svgload_buffer(content.as_str().encode("utf-8")) as image: image.write_to_file(filename) return filename elif fileformat: if fileformat == "svg": buf = io.BytesIO(content.as_str().encode("utf-8")) buf.seek(0) elif fileformat in ["jpg", "png"]: with pyvips.Image.svgload_buffer(content.as_str().encode("utf-8")) as image: data = image.write_to_buffer("." + fileformat) buf = io.BytesIO(data) buf.seek(0) return StreamingResponse(content=buf, media_type=MEDIA_TYPES[fileformat]) if __name__ == "__main__": uvicorn.run("speckles:app", workers=3, port=8099)