Compare commits

...

10 commits

Author SHA1 Message Date
0e2f8a55ce
View to list all images in collection (#3)
* Cache generation instead of views

This way, query params are kept cached.

* Add view to list all images
2024-09-15 21:41:06 +02:00
b859fece88
Introduce image request query parameter for filtering and selection (#2)
* Add selection ability using image query parameter

the selections is the index of the list of images,
where 0 is the first image. `os.listdir` is asumed
to always sort the list the same.

- if no selection is provided, take random image
  from image collection (like main)
- if 1 selection is provided, return the nth image
  from collection
- if 2 or more selections are provided, take random
  item from the selection and return the image at
  that index.

* Handle bad selection input

- Nondigit selections returns HTTP 401
- exclude indexes out of range (0--collection length)
2024-09-15 20:46:02 +02:00
90638fd72a
Remove outdated line in README.md 2024-09-09 10:50:51 +02:00
ee699df9e6
Improve image rendering (#1)
* Re-randomize image if dimensions do not satisfy

* Try a maximum of 10 times to generate an image

* Decrease image generation cache to 1s

* Increase image quality
2024-09-09 09:12:44 +02:00
e124ce6501
Fix typos in README 2024-09-03 12:05:32 +02:00
d0c09b628b
Fix wrong port number in Caddyfile 2024-09-03 12:01:33 +02:00
72f3505e7b
Fix inconsistent whitespace in Caddyfile 2024-09-03 09:33:03 +02:00
486990c5ed Restore waitress defaults 2024-09-03 09:12:04 +02:00
e79c29fdcd Improve example links 2024-09-03 01:17:06 +02:00
c289c2591a
Markdown proof archived links in README.md 2024-09-03 01:14:55 +02:00
7 changed files with 133 additions and 36 deletions

View file

@ -1,4 +1,4 @@
placeany.site {
encode zstd gzip
reverse_proxy http://localhost:5099
encode zstd gzip
reverse_proxy http://localhost:8080
}

View file

@ -12,5 +12,5 @@ COPY wsgi.py wsgi.py
VOLUME images
ENTRYPOINT waitress-serve --host 127.0.0.1 --port 5099 wsgi:app
ENTRYPOINT waitress-serve wsgi:app

View file

@ -14,5 +14,5 @@ FROM app AS imgs
ARG images
copy ${images} images
ENTRYPOINT waitress-serve --host 127.0.0.1 --port 5099 wsgi:app
ENTRYPOINT waitress-serve wsgi:app

View file

@ -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
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
[Horse_ebookmarklet]([http://www.heyben.com/horse_ebookmarklet/](https://web.archive.org/web/20120223050454/http://www.heyben.com/horse_ebookmarklet/)).
Inspirations:
- 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
@ -22,29 +32,43 @@ First, create an image collection.
1. Go to the code: `cd path/to/holder`. Copy `images` folder to it.
1. Create and activate a virtualenv.
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. Go to [http://localhost:5099](http://localhost:5099) in your web browser.
1. Start the app: `waitress-serve wsgi:app`
1. Go to [http://localhost:8080](http://localhost:8080) in your web browser.
1. Done!
### Run as Container
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
instead used.
In this build, waitress is used for production readyness.
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
Containerfile.
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
http://localhost:5000/200/300
A reverse proxy in front of placeany is recommended. An example
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
http://localhost:5000/g/200/300
## Podman tip: generate systemd files
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.

View file

@ -37,10 +37,9 @@
<h1>placeany</h1>
<p>A quick and simple service for getting pictures for use as placeholders in your designs or code.
Just put your image size (width &amp; height) after our URL and you'll get a placeholder.</p>
<pre><output>Examples:
<a href="/200/300">{{ url }}/200/300</a>
<a href="/g/200/300">{{ url }}/g/200/300</a>
</output></pre>
<output>Like this: <a href="/200/300">{{ url }}/200/300</a>
<br>or: <a href="/g/200/300">{{ url }}/g/200/300</a>
</output>
</article>
<div class="unit">
<img src="/450/230" alt="">
@ -67,4 +66,4 @@
</footer>
</body>
</html>
</html>

47
templates/list.html Normal file
View 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&amp;image=n2&amp;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>

53
wsgi.py
View file

@ -2,7 +2,7 @@ import os
import random
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 PIL import Image, ImageOps
@ -10,6 +10,7 @@ app = Flask(__name__)
GREY = "G"
COLOR = "RGB"
IMAGE_DIR = "./images"
cache = Cache(config={"CACHE_TYPE": "SimpleCache"})
@ -17,23 +18,45 @@ app = Flask(__name__)
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"""
im_src = random.choice(os.listdir("./images"))
im = Image.open(f"images/{im_src}")
if retries > 10:
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()
max_x, max_y = im.size
if x < max_x or y < max_y:
im = ImageOps.fit(im, (x, y))
if x > max_x and y > max_y:
return get_cropped_image(x, y, grey, retries + 1)
im = ImageOps.fit(im, (x, y))
if grey:
im = ImageOps.grayscale(im)
im.save(out, "WEBP", quality=50)
im.save(out, "WEBP", quality=80)
out.seek(0)
return out
def make_response(x, y, color_mode=COLOR):
im = get_cropped_image(x, y, color_mode == GREY)
def make_response(x, y, s, color_mode=COLOR):
im = get_cropped_image(x, y, s, color_mode == GREY)
if not im:
return Response(status=401)
return send_file(im, mimetype="image/webp")
@ -49,16 +72,20 @@ def bookmarklet():
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>")
@cache.cached(10)
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>")
@cache.cached(10)
def generate_grey(x, y):
return make_response(x, y, GREY)
return make_response(x, y, request.args.getlist("image"), GREY)
if __name__ == "__main__":