speckles/speckles.py
2024-10-27 20:46:39 +01:00

309 lines
8.7 KiB
Python

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)