dwyl / imgup

🌅 Effortless image uploads to AWS S3 with automatic resizing including REST API.
https://imgup.fly.dev/
107 stars 19 forks source link

Feat: Dynamic Image Resizing #99

Open nelsonic opened 1 year ago

nelsonic commented 1 year ago

At present we have single size image resizing: 7. Resizing/compressing files This is a very good starting point, 👍 but it sets and artificial constraint that ends up looking "meh" on most devices.

ginger-compressed

image

Story

As a person using multiple devices to view the App - that features image content - I want images to conform to the device/screen size So that they always look their best.

As noted in https://github.com/dwyl/imgup/issues/91 our friend has created https://github.com/jupiter/rust-image-worker which appears to be well-documented and tested and runs on Cloudflare with CDN caching. 🏎️ While it currently does not have support for certain formats https://github.com/jupiter/rust-image-worker/issues/3 But the underlying library does support GIF and Webp: https://docs.rs/image/latest/image/enum.ImageFormat.html#variant.Gif

Todo

ndrean commented 1 year ago

@nelsonic I am curious. ,You must probably have read this article. Since (obviously) you are using a server to upload image, what is the reason you did not consider to run your own Task to compress the image before saving to S3? You would save a bucket and a lambda. Something obvious I am missing?

nelsonic commented 1 year ago

@ndrean hadn't seen that fly.io blog post published Mar 13, 2023; thanks for sharing. image image image

It's quite superficial, not really a "tutorial", more of a "you can do this thing" but you have figure out the precise details on your own ... 🙃 That's exactly the kind of post that mega frustrates me.

To answer your question: we want to maintain the original image so that people can view as much detail as possible. We will save money when we switch to using B2 https://github.com/dwyl/imgup/issues/98 I don't have anything against optimising the images in a Task in Elixir. But I would prefer to minimise the Elixir bottleneck and use an existing Rust project to the just-in-time optimisation. From an experience perspective the Elixir approach where you perform the immediate resizing could be better in terms of response time. ⏳ We could test it. For now the S3 buckets + Lambda function works "OK". Have you implemented image compression/resizing in Elixir? how fast was it?

Ultimately using B2 + Cloudflare which is "free" for their CDN would save us money (over S3 + Lambda)

ndrean commented 1 year ago

@nelsonic, Yes superficial indeed, but it gives ideas.

But aren't you serving the client back with a compressed image? https://user-images.githubusercontent.com/17494745/242736344-bd61d716-8a4e-445f-a643-8f5d13a00510.png

that's why I imagined it was an extra step (and I always fear AWS costs....).

I forked the repo but did not run it yet but I plan to as I never did this server-side nor loaded files yet with Phoenix. But I have no intention so far to send something to S3, too much bills with AWS.

I maybe totally wrong but I thought a simple URL.createObjectURL for the preview was enough, although I have no idea if Liveview accepts this.

I will also try the https://hexdocs.pm/image/Image.html and explore

nelsonic commented 1 year ago

Yeah, the current AWS bill is currently Zero because it's all in the "Free Tier". Which is why the priority on #98 is priority-2 Returning the compressed image URL is a stop-gap and will be phased out in due course instead the device will request the size of image that best fits the viewport #91 therefore prospectively resizing becomes wasteful. If the device uploading the image is 600px wide there's no need to have images 200px or 2000px wide. Then when subsequent devices attempt to view the image, if they can request dimensions that best suit their viewport without a significant latency penalty, that's super desirable.

Very curious to see your exploration of the https://github.com/elixir-image/image library. 👌 I didn't find the docs to be particularly beginner-friendly when I tried to read them a few months ago ... 🤷‍♂️

I'm especially not a huge fan of risk of crashing the whole BEAM with one malformed image: https://hexdocs.pm/image/readme.html#security-considerations

image

If I can offload image processing/resizing to a totally separate Rust micro-app I'm going to chose that every time. 🚀 But if I needed to do Image recognition in Elixir then the image library looks interesting. 💭

ndrean commented 1 year ago

@nelsonic I am exploring 2 ideas. If this does not interest you, just tell me (nicely 😄). I send the image from the browser (not from PHoenix) to a (tiny) companion Node.js server that runs sharp,. The server returns a resized image based on the device. You then PUT directly to S3 via an API Gateway, no round trip to the PHoenix server, and the bucket is closed, except this endpoint that exposes him, of course.

1) The Node.js server (30 LOC) runs independently next to the Phoenix app. It includes sharp. You define an endpoint to which the Phoenix app can POST the image as well as the desired dimensions you collect from the device (eg window.innerHeight * 0.5). You resize the image accordingly. On completion, it will respond back. I am fighting with how to display a preview of the result back.

2) It seems that sending the image to Phoenix is consuming resources but you don't perform anything on the image. You can send the image directly to S3 via an API Gateway, no round trip. The whole thing takes a few LOC (including the "CIDing of the filename and saving the metadata of the file to the Phoenix database). I am fighting with a CORS problem though. If you are interested, I can append some code here.

nelsonic commented 1 year ago

@ndrean these are all perfectly valid suggestions and we have done something similar in the past.

our goal with doing this in Phoenix is to minimise latency. In the best case scenario API Gateway + Lambda is about 300ms round trip. Much worse if the Lambda function has to “cold” boot 🥶 Direct Upload to S3 works and we’ve done that before too.

Perhaps the most important thing we aren’t yet doing in the imgup app is logging metadata and person info so that the people making the uploads can see all their images easily. in other words, try to think of where an image uploading service with privacy focus can go rather than just looking at what we currently already have. to be clear: I agree that we don’t need Phoenix. Heck we could achieve what we currently have with a single PHP file/script copy-pasted from StackOverflow or ChatGPT … But when it comes to a roadmap that heavily features images, having a clear grasp of the stack is mega important.

ndrean commented 1 year ago

@nelsonic Of course, I do not pretend any originality at all. Just that I found the Phoenix code difficult. When you say "300ms round trip", you mean between upload and the display of the URL and preview of their pics?

Perhaps the most important thing we aren’t yet doing in the imgup app is logging metadata and person info so that the people making the uploads can see all their images easily.

I probably misunderstood. You mean a profile to be able to join his URLs and meta, thus retrieve?

nelsonic commented 1 year ago

Yeah, for now this repo is “just” a simple way to upload images. If we had time we would build out the rest of the features. 💭

ndrean commented 1 year ago

I imagine you want to use the "standard" mix .phx.gen.auth and a join table of S3 URLs. The tricky part is probably to handle safely thousands of downloads S3 -> client.

nelsonic commented 1 year ago

Indeed. The “standard”, though woefully incomplete, mix phx.gen.auth is what we are using as the basis for our auth re-write which is ongoing … 👌 Then imgup will have one-tap auth and images will be associated with a person_id so people can easily see & share the images they’ve uploaded. ✅

ndrean commented 1 year ago

You probably know https://imagekit.io/ ? I found interesting the way they did it, and rather inline with your roadmap,

Watch https://www.youtube.com/watch?v=sWcSYG1eifo

Screenshot 2023-08-29 at 17 18 50

however, the price seems high.

Screenshot 2023-08-29 at 17 25 16
ndrean commented 1 year ago

@nelsonic I believe I have a use case for a modal in the following situation: once you uploaded files, you have a list or miniatures previews of them. You can put a modal for each file to 1) link to a new page 2) a form to enter a name and download the file. What do you think?

nelsonic commented 1 year ago

Respectfully, I disagree. 🙅 Unless you need to hijack the person's attention for very specific reason, Modals are never the answer to "How do we make excellent UX here?" If you've ever watched a senior citizen use the web they are always confused by them.

Interacting with a bank of images needs to be as "flat" as possible. Inserting a new image into the DOM and applying a highlight (border) around it is enough to show the recently uploaded file. Opening the larger version of that file should be full-screen with a gentle transition and a "back button" to allow them to return to the list.

Apologies if I come across as "harsh" (bordering on militant) but Modals are evil and should be avoided at all costs. 😉

ndrean commented 1 year ago

I used Vix to compress/resize (Vix.Vips.Image and Vix.Vips.Operation). It is easy and super fast.. You can do this in the clientless flow in the handle_progress

A JPEG 5472x3648 of 2.5MB action

compressed to 988kB (Q=30): small

resized 10%: to 547x365 (43kB): small

and a thumb of 5kB with the same ratio 1.5 thumb

ndrean commented 1 year ago

A guide: https://imagekit.io/blog/how-to-resize-image-in-html/

Screenshot 2023-09-03 at 12 34 11
ndrean commented 1 year ago

@nelsonic

Let me know what you think of this? it is only SSR because Phoenix transforms the images.

When it reads a picture, I transform it into WEBP format to minimise the size. Only WEBP fomat are saved in S3.

You can let the browser resize the pic for you, or minimise the data over the wire and resize it. Resize to what? To the uploading device for example?

With this 100% Phoenix strategy, we could run a job to compute the resizing for different pre-defined devices, upload this to S3. You will have several versions of the same pic in S3. Then - upon some matching - when you want to display a picture, you "fetch" the right format depending upon your device.

To keep it simple - and given I don't want to pay for - , if you upload a pic from a device, the image will be resized to the dimension of the device meaning that the S3 upload is adapted to your device. I do not keep the original format, just because this is a demo, but it is possible.

https://up-image.fly.dev

This kind of app - with the jungle of the async messaging I used - could benefit of the new assign_async coming in LiveView 0.20: see https://www.youtube.com/watch?v=FADQAnq0RpA

I tried to use it when you upload several files. Each upload will trigger a "writter" and then you invoke a start_async and handle_async. This works for one upload. Indeed, each task is identified by its own unique key. If you upload several files, this will be triggered several times and invoke the same key for this async task, so this will fail. In such case, a "traditionnal" Task with the handle_info handlers.