Photo editor
What it does
This example is a small photo editor. You can upload a picture,
shrink it, rotate it, convert it to grayscale, and download the
result. The app demonstrates the whole life cycle of an image in a
Drafter app: the image comes in through a FileUpload, lives in the
state as a Picture, is changed by transformations, and goes out
through a Download.
Try it
Upload any image (or play with the starting swatch), then stack some transformations.
from drafter import *
from dataclasses import dataclass
@dataclass
class State:
photo: Picture
@route
def index(state: State) -> Page:
return Page(state, [
Header("Photo Editor"),
"Current photo:\n",
state.photo,
"\n",
Row("New image file:", FileUpload("new_photo", accept="image/*")),
Button("Upload", "update_photo"),
"\n",
Button("Shrink", "shrink_photo"),
Button("Rotate", "rotate_photo"),
Button("Grayscale", "grayscale_photo"),
"\n",
Download("Download the photo", "photo.png", state.photo)
])
@route
def update_photo(state: State, new_photo: Picture | None) -> Page:
if new_photo is not None:
state.photo = new_photo
return index(state)
@route
def shrink_photo(state: State) -> Page:
state.photo = state.photo.scale(0.5)
return index(state)
@route
def rotate_photo(state: State) -> Page:
state.photo = state.photo.rotate(90)
return index(state)
@route
def grayscale_photo(state: State) -> Page:
state.photo = state.photo.grayscale()
return index(state)
assert_equal(
shrink_photo(State(Picture.new(40, 20, "salmon"))),
index(State(Picture.new(20, 10, "salmon"))))
start_server(State(Picture.new(60, 40, "salmon")))
The code
The state holds exactly one thing: the current Picture. Every
route follows the same pattern: it replaces state.photo with a
transformed version, then shows the page again. update_photo is
the only route with a parameter, because it is the only route that
receives a value from a form.
How it works
Three details about types make this app work:
- A
Picturein page content displays itself. The linestate.photoin the content list renders as an image, with noImagecomponent required (though Image exists for more control). new_photo: Picture | Noneis how uploads become pictures. Annotating the parameter asPicturemakes Drafter decode the uploaded file into one. The| Nonepart means that pressing Upload with no file chosen deliversNoneinstead of raising an error, and the route checks forNonebefore replacing anything.- Transformations return new pictures.
scale,rotate, andgrayscaledo not change a picture in place; they return a changed copy, which is why every route assigns:state.photo = state.photo.scale(0.5).
The test at the bottom compares whole pages built from two states,
which works because Pictures compare by their actual pixels: a
40x20 salmon swatch scaled by half equals a 20x10 salmon swatch
made from scratch.
Make it yours
- Modify: rotate by 45 instead of 90 and see what happens to the corners.
- Modify: add a "Tiny" button that scales by 0.1, and an "Undo-ish" button that scales by 2.0 (and notice why that is not really undo).
- Complete: add a
flipbutton; check the Picture reference for the method. - Combine: keep
original: Picturein state alongsidephoto, set it on upload, and add a real "Reset" button. - Create: build a meme maker by combining this app with a
TextBoxand the text-drawing tools from the Picture reference.
Tests
One assert_equal covers the transformation pipeline: shrinking a
known swatch produces exactly the page that a half-size swatch would
produce. Pixel-level equality makes image code surprisingly
testable. In your own tests, build small Picture.new(...) swatches
rather than loading files.
Likely errors
- Uploading a non-image: a file that cannot be decoded as an
image stops the route with a friendly conversion error naming the
parameter. The
accept="image/*"on theFileUploadsteers the file picker toward images but is a convenience, not a guarantee. - Forgetting the
| None: with a plainPictureannotation, pressing Upload with no file chosen raises an error instead of quietly doing nothing. - Expecting transformations to mutate:
state.photo.rotate(90)alone does nothing visible; without the assignment, the rotated copy is thrown away. - Deploying and losing the photo: the state resets on reload, as it always does, so a deployed editor starts from the built-in swatch rather than from anything a visitor uploaded earlier.
Related
- Use pictures is the how-to guide for images from start to finish.
- Picture documents every transformation and constructor.
- FileUpload and Download are the components that bring files in and send them out.