speckles/speckles.py

307 lines
8.6 KiB
Python
Raw Normal View History

2023-05-18 14:15:37 +00:00
import itertools
2024-10-27 18:46:19 +00:00
import io
from fastapi.responses import StreamingResponse
2023-05-18 14:15:37 +00:00
from pathlib import Path
import pyvips
import logging
from perlin import CoordsGenerator
import svg
2023-05-18 14:15:37 +00:00
import numpy as np
2024-10-27 18:46:19 +00:00
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=["*"],
)
2023-05-18 14:15:37 +00:00
RGen = np.random.default_rng()
2023-05-18 14:15:37 +00:00
2024-10-27 18:46:19 +00:00
MEDIA_TYPES = {
"png": "image/png",
"jpeg": "image/jpeg",
"jpg": "image/jpeg",
"svg": "image/svg+xml",
"pdf": "application/pdf",
}
2024-10-27 15:42:02 +00:00
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)],
)
2024-10-27 15:42:02 +00:00
class Include(svg.Element):
element_name = "svg"
transform: list[svg.Transform] | None = None
2024-10-27 15:42:02 +00:00
def __str__(self) -> str:
return self.text
2023-05-18 14:15:37 +00:00
2024-10-27 15:42:02 +00:00
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)
]
2024-10-27 15:42:02 +00:00
else:
return svg.Use(
href=f"#{marker}",
transform=[svg.Translate(x=x, y=y), svg.Scale(np.sqrt(s) / 100)],
)
2023-05-18 14:15:37 +00:00
2024-10-27 18:46:19 +00:00
@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, color=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(
[
2024-10-27 15:42:02 +00:00
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))
]
)
2023-05-18 14:15:37 +00:00
)
elements.append(style)
2024-10-27 15:42:02 +00:00
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)
2024-10-27 18:46:19 +00:00
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:
2024-10-27 19:09:57 +00:00
if fileformat == "svg":
buf = io.BytesIO(content.as_str().encode("utf-8"))
buf.seek(0)
else:
with pyvips.Image.svgload_buffer(content.as_str().encode("utf-8")) as image:
buf = image.write_to_buffer(fileformat)
2024-10-27 18:46:19 +00:00
return StreamingResponse(content=buf, media_type=MEDIA_TYPES[fileformat])
2023-05-18 14:15:37 +00:00
2023-07-10 07:14:05 +00:00
if __name__ == "__main__":
2024-10-27 18:46:19 +00:00
uvicorn.run("speckles:app", workers=3, port=8099)