Compare commits
10 commits
f468ec5b6a
...
0e2f8a55ce
| Author | SHA1 | Date | |
|---|---|---|---|
| 0e2f8a55ce | |||
| b859fece88 | |||
| 90638fd72a | |||
| ee699df9e6 | |||
| e124ce6501 | |||
| d0c09b628b | |||
| 72f3505e7b | |||
| 486990c5ed | |||
| e79c29fdcd | |||
| c289c2591a |
7 changed files with 133 additions and 36 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
placeany.site {
|
placeany.site {
|
||||||
encode zstd gzip
|
encode zstd gzip
|
||||||
reverse_proxy http://localhost:5099
|
reverse_proxy http://localhost:8080
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,5 +12,5 @@ COPY wsgi.py wsgi.py
|
||||||
|
|
||||||
VOLUME images
|
VOLUME images
|
||||||
|
|
||||||
ENTRYPOINT waitress-serve --host 127.0.0.1 --port 5099 wsgi:app
|
ENTRYPOINT waitress-serve wsgi:app
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -14,5 +14,5 @@ FROM app AS imgs
|
||||||
ARG images
|
ARG images
|
||||||
copy ${images} images
|
copy ${images} images
|
||||||
|
|
||||||
ENTRYPOINT waitress-serve --host 127.0.0.1 --port 5099 wsgi:app
|
ENTRYPOINT waitress-serve wsgi:app
|
||||||
|
|
||||||
|
|
|
||||||
52
README.md
52
README.md
|
|
@ -4,10 +4,20 @@ A quick and simple service for getting pictures of whatever-you-want
|
||||||
for use as placeholders in your designs or code. Just put your image
|
for use as placeholders in your designs or code. Just put your image
|
||||||
size (width & height) after the URL and you'll get a placeholder.
|
size (width & height) after the URL and you'll get a placeholder.
|
||||||
|
|
||||||
Similar URL API as [Placekitten]([http://placekitten.com](https://web.archive.org/web/20110504042732/http://placekitten.com/)).
|
There is also a bookmarklet service which enhances sites with many images.
|
||||||
|
|
||||||
There is also a bookmarklet service which works the same as
|
Inspirations:
|
||||||
[Horse_ebookmarklet]([http://www.heyben.com/horse_ebookmarklet/](https://web.archive.org/web/20120223050454/http://www.heyben.com/horse_ebookmarklet/)).
|
|
||||||
|
- https://web.archive.org/web/20110504042732/http://placekitten.com/
|
||||||
|
- https://web.archive.org/web/20120223050454/http://www.heyben.com/horse_ebookmarklet/.
|
||||||
|
|
||||||
|
## Example calls
|
||||||
|
|
||||||
|
Generates an image, 200px wide and 300px tall:
|
||||||
|
http://localhost:8080/200/300
|
||||||
|
|
||||||
|
Generates an image in grayscale, 200px wide and 300px tall:
|
||||||
|
http://localhost:8080/g/200/300
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
|
|
@ -22,29 +32,43 @@ First, create an image collection.
|
||||||
1. Go to the code: `cd path/to/holder`. Copy `images` folder to it.
|
1. Go to the code: `cd path/to/holder`. Copy `images` folder to it.
|
||||||
1. Create and activate a virtualenv.
|
1. Create and activate a virtualenv.
|
||||||
1. Get dependencies in place: `pip install -r requirements.txt`
|
1. Get dependencies in place: `pip install -r requirements.txt`
|
||||||
1. Start the app: `waitress-serve --host 127.0.0.1 --port 5099 wsgi:app`
|
1. Start the app: `waitress-serve wsgi:app`
|
||||||
1. Go to [http://localhost:5099](http://localhost:5099) in your web browser.
|
1. Go to [http://localhost:8080](http://localhost:8080) in your web browser.
|
||||||
1. Done!
|
1. Done!
|
||||||
|
|
||||||
### Run as Container
|
### Run as Container
|
||||||
|
|
||||||
The most easy and portable way to use this is to use Docker or Podman.
|
The most easy and portable way to use this is to use Docker or Podman.
|
||||||
In this build, waitress is used for production readyness. Port 5099 is
|
In this build, waitress is used for production readyness.
|
||||||
instead used.
|
|
||||||
|
|
||||||
podman build .
|
podman build .
|
||||||
podman run -it -p 5099:5099 -v ./images:/app/images <container id>
|
podman run -it -p 8080:8080 -v ./images:/app/images <container id>
|
||||||
|
|
||||||
If you wish to embed images in container as well, use alternate
|
If you wish to embed images in container as well, use alternate
|
||||||
Containerfile.
|
Containerfile.
|
||||||
|
|
||||||
podman build -f Containerfile.aio --build-arg images=./images .
|
podman build -f Containerfile.aio --build-arg images=./images .
|
||||||
podman run -it -p 5099:5099 <container id>
|
podman run -it -p 8080:8080 <container id>
|
||||||
|
|
||||||
## Example calls
|
### Run behind reverse proxy
|
||||||
|
|
||||||
# generates an image, 200px wide and 300px tall
|
A reverse proxy in front of placeany is recommended. An example
|
||||||
http://localhost:5000/200/300
|
Caddyfile is available to make https "just work", but Nginx+certbot
|
||||||
|
will be equally fine.
|
||||||
|
|
||||||
# generates an image in grayscale, 200px wide and 300px tall
|
## Podman tip: generate systemd files
|
||||||
http://localhost:5000/g/200/300
|
|
||||||
|
Make sure to enable lingering user processes.
|
||||||
|
|
||||||
|
loginctl enable-linger $USER
|
||||||
|
|
||||||
|
Then, create and change directory to systemd.
|
||||||
|
|
||||||
|
mkdir -p .config/systemd/user
|
||||||
|
cd .config/systemd/user
|
||||||
|
|
||||||
|
Now, generate the systemd user service.
|
||||||
|
|
||||||
|
podman generate systemd --new -f -n placeany
|
||||||
|
|
||||||
|
Your container can now be enabled and started.
|
||||||
|
|
|
||||||
|
|
@ -37,10 +37,9 @@
|
||||||
<h1>placeany</h1>
|
<h1>placeany</h1>
|
||||||
<p>A quick and simple service for getting pictures for use as placeholders in your designs or code.
|
<p>A quick and simple service for getting pictures for use as placeholders in your designs or code.
|
||||||
Just put your image size (width & height) after our URL and you'll get a placeholder.</p>
|
Just put your image size (width & height) after our URL and you'll get a placeholder.</p>
|
||||||
<pre><output>Examples:
|
<output>Like this: <a href="/200/300">{{ url }}/200/300</a>
|
||||||
<a href="/200/300">{{ url }}/200/300</a>
|
<br>or: <a href="/g/200/300">{{ url }}/g/200/300</a>
|
||||||
<a href="/g/200/300">{{ url }}/g/200/300</a>
|
</output>
|
||||||
</output></pre>
|
|
||||||
</article>
|
</article>
|
||||||
<div class="unit">
|
<div class="unit">
|
||||||
<img src="/450/230" alt="">
|
<img src="/450/230" alt="">
|
||||||
|
|
|
||||||
47
templates/list.html
Normal file
47
templates/list.html
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>placeany</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
width: 900px;
|
||||||
|
margin: 100px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.grid {
|
||||||
|
display: flex;
|
||||||
|
gap: 1em;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<h1>placeany</h1>
|
||||||
|
<p>The following images are used on this placeany instance.
|
||||||
|
You may request a specific image by adding <b>?image=n</b> to your request
|
||||||
|
(where <b>n</b> is the number of the image you want), or filter the options
|
||||||
|
by adding multiple values like this: <b>?image=n1x&image=n2&image=n3</b>.</p>
|
||||||
|
<div class="grid">
|
||||||
|
{% for n in range(count) %}
|
||||||
|
<div class="unit">
|
||||||
|
<img src="/125/125?image={{ n }}" alt="">
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
<hr>
|
||||||
|
<footer role="contentinfo">
|
||||||
|
Also available as a <a href="/bookmarklet">Bookmarklet Service</a>.<br>
|
||||||
|
Source on <a href="https://github.com/madr/placeany">GitHub</a>.
|
||||||
|
</footer>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
51
wsgi.py
51
wsgi.py
|
|
@ -2,7 +2,7 @@ import os
|
||||||
import random
|
import random
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from flask import Flask, render_template, request, send_file
|
from flask import Flask, Response, render_template, request, send_file
|
||||||
from flask_caching import Cache
|
from flask_caching import Cache
|
||||||
from PIL import Image, ImageOps
|
from PIL import Image, ImageOps
|
||||||
|
|
||||||
|
|
@ -10,6 +10,7 @@ app = Flask(__name__)
|
||||||
|
|
||||||
GREY = "G"
|
GREY = "G"
|
||||||
COLOR = "RGB"
|
COLOR = "RGB"
|
||||||
|
IMAGE_DIR = "./images"
|
||||||
|
|
||||||
cache = Cache(config={"CACHE_TYPE": "SimpleCache"})
|
cache = Cache(config={"CACHE_TYPE": "SimpleCache"})
|
||||||
|
|
||||||
|
|
@ -17,23 +18,45 @@ app = Flask(__name__)
|
||||||
cache.init_app(app)
|
cache.init_app(app)
|
||||||
|
|
||||||
|
|
||||||
def get_cropped_image(x, y, grey=False):
|
@cache.memoize(1)
|
||||||
|
def get_cropped_image(x, y, s, grey=False, retries=0):
|
||||||
"""crops a random image from collection"""
|
"""crops a random image from collection"""
|
||||||
im_src = random.choice(os.listdir("./images"))
|
if retries > 10:
|
||||||
im = Image.open(f"images/{im_src}")
|
return None
|
||||||
|
options = os.listdir(IMAGE_DIR)
|
||||||
|
try:
|
||||||
|
selection = list(
|
||||||
|
filter(
|
||||||
|
lambda i: i in range(0, len(options)),
|
||||||
|
map(int, s),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except ValueError:
|
||||||
|
return None
|
||||||
|
match len(selection):
|
||||||
|
case 0:
|
||||||
|
im_src = random.choice(options)
|
||||||
|
case 1:
|
||||||
|
im_src = options[selection[0]]
|
||||||
|
case _:
|
||||||
|
im_src = options[random.choice(selection)]
|
||||||
|
im = Image.open(f"{IMAGE_DIR}/{im_src}")
|
||||||
out = BytesIO()
|
out = BytesIO()
|
||||||
max_x, max_y = im.size
|
max_x, max_y = im.size
|
||||||
if x < max_x or y < max_y:
|
if x > max_x and y > max_y:
|
||||||
|
return get_cropped_image(x, y, grey, retries + 1)
|
||||||
im = ImageOps.fit(im, (x, y))
|
im = ImageOps.fit(im, (x, y))
|
||||||
if grey:
|
if grey:
|
||||||
im = ImageOps.grayscale(im)
|
im = ImageOps.grayscale(im)
|
||||||
im.save(out, "WEBP", quality=50)
|
im.save(out, "WEBP", quality=80)
|
||||||
out.seek(0)
|
out.seek(0)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
def make_response(x, y, color_mode=COLOR):
|
def make_response(x, y, s, color_mode=COLOR):
|
||||||
im = get_cropped_image(x, y, color_mode == GREY)
|
im = get_cropped_image(x, y, s, color_mode == GREY)
|
||||||
|
if not im:
|
||||||
|
return Response(status=401)
|
||||||
return send_file(im, mimetype="image/webp")
|
return send_file(im, mimetype="image/webp")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -49,16 +72,20 @@ def bookmarklet():
|
||||||
return render_template("bookmarklet.html", url=u)
|
return render_template("bookmarklet.html", url=u)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route("/images")
|
||||||
|
def collection():
|
||||||
|
c = len(os.listdir(IMAGE_DIR))
|
||||||
|
return render_template("list.html", count=c)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/<int:x>/<int:y>")
|
@app.route("/<int:x>/<int:y>")
|
||||||
@cache.cached(10)
|
|
||||||
def generate(x, y):
|
def generate(x, y):
|
||||||
return make_response(x, y, COLOR)
|
return make_response(x, y, request.args.getlist("image"), COLOR)
|
||||||
|
|
||||||
|
|
||||||
@app.route("/g/<int:x>/<int:y>")
|
@app.route("/g/<int:x>/<int:y>")
|
||||||
@cache.cached(10)
|
|
||||||
def generate_grey(x, y):
|
def generate_grey(x, y):
|
||||||
return make_response(x, y, GREY)
|
return make_response(x, y, request.args.getlist("image"), GREY)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue