App-BlurFill
view release on metacpan or search on metacpan
lib/App/BlurFill/Web.pm view on Meta::CPAN
The image file to be processed. This parameter is required.
It should be a valid image file format (e.g., JPEG, PNG, GIF).
=head2 width
The desired width of the output image. Default is 650 pixels.
=head2 height
The desired height of the output image. Default is 350 pixels.
=head1 EXAMPLE
POST /blur
Content-Type: multipart/form-data
image: <binary image data>
width: 800
height: 600
=head2 Using C<curl>
# This will return HTML with the results page
curl -X POST -F "image=@path/to/image.jpg" -F "width=800" -F "height=600" http://localhost:3000/blur
# To download the image directly
curl -OJ http://localhost:3000/download/image_blur.png
=head1 RESPONSE
The POST /blur response will be an HTML page displaying the blurred image with
download options. The GET /download/:filename response will be the actual image file.
=cut
use v5.40;
package App::BlurFill::Web;
use Dancer2;
our $VERSION = '0.0.5';
use File::Temp qw(tempfile tempdir);
use File::Spec;
use File::Copy;
use App::BlurFill;
# Create a persistent temp directory for storing processed images
my $TEMP_DIR = File::Temp::tempdir(CLEANUP => 1);
sub _get_css {
return <<'CSS';
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
max-width: 600px;
width: 100%;
padding: 40px;
}
h1 {
color: #333;
font-size: 32px;
font-weight: 700;
margin-bottom: 12px;
text-align: center;
}
.subtitle {
color: #666;
font-size: 16px;
text-align: center;
margin-bottom: 32px;
line-height: 1.5;
}
.form-group {
margin-bottom: 24px;
}
label {
display: block;
color: #444;
font-weight: 600;
margin-bottom: 8px;
font-size: 14px;
}
input[type="file"] {
display: block;
width: 100%;
padding: 12px;
border: 2px dashed #667eea;
border-radius: 8px;
background: #f8f9ff;
cursor: pointer;
transition: all 0.3s ease;
}
input[type="file"]:hover {
border-color: #764ba2;
background: #f0f1ff;
}
input[type="number"] {
width: 100%;
padding: 12px 16px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 16px;
transition: all 0.3s ease;
}
input[type="number"]:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.dimensions {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
button, .button {
width: 100%;
padding: 16px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
border-radius: 8px;
font-size: 18px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
text-decoration: none;
display: inline-block;
text-align: center;
}
button:hover, .button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.5);
}
button:active, .button:active {
transform: translateY(0);
}
.info {
background: #f8f9ff;
border-left: 4px solid #667eea;
padding: 16px;
margin-top: 24px;
border-radius: 4px;
}
.info p {
color: #555;
font-size: 14px;
line-height: 1.6;
margin-bottom: 8px;
}
.info p:last-child {
margin-bottom: 0;
}
.info strong {
color: #333;
}
.credits {
padding-top: 1em;
color: #999;
text-align: center;
}
.credits a:link {
color: #c9c;
}
.credits a:visited {
color: #969;
}
.result-image {
margin: 24px 0;
text-align: center;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.result-image img {
max-width: 100%;
height: auto;
display: block;
}
.success-message {
background: #f0fdf4;
border-left: 4px solid #10b981;
padding: 16px;
margin-bottom: 24px;
border-radius: 4px;
color: #065f46;
}
.action-buttons {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: 24px;
}
.button-secondary {
background: linear-gradient(135deg, #6b7280 0%, #4b5563 100%);
}
@media (max-width: 640px) {
.container {
padding: 24px;
}
h1 {
font-size: 24px;
}
.dimensions, .action-buttons {
grid-template-columns: 1fr;
}
}
CSS
}
get '/' => sub {
my $css = _get_css();
return <<"HTML";
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BlurFill - Perfect crops, zero letterboxing: smart blur-fill from your source image.</title>
<style>
$css
</style>
</head>
<body>
<div class="container">
<h1>BlurFill</h1>
<p class="subtitle">Perfect crops, zero letterboxing: smart blur-fill from your source image.</p>
<form action="/blur" method="POST" enctype="multipart/form-data">
<div class="form-group">
<label for="image">Select Image</label>
<input type="file" id="image" name="image" accept="image/jpeg,image/jpg,image/png,image/gif" required>
</div>
<div class="form-group">
<label>Output Dimensions</label>
<div class="dimensions">
<div>
<label for="width">Width (px)</label>
<input type="number" id="width" name="width" value="650" min="1" max="4000">
</div>
<div>
<label for="height">Height (px)</label>
<input type="number" id="height" name="height" value="350" min="1" max="4000">
</div>
</div>
</div>
<button type="submit">Generate resized image</button>
</form>
<div class="info">
<p><strong>How it works:</strong></p>
<p>1. Upload your image (JPEG, PNG, or GIF)</p>
<p>2. Set your desired output dimensions</p>
<p>3. Click "Generate" to create a resized image with your source image centered and filled</p>
<p>4. Your processed image will be displayed with a download link</p>
</div>
<div class="credits">
Version $VERSION /
Made by <a href="https://links.davecross.co.uk/">Dave Cross</a> /
( run in 1.627 second using v1.01-cache-2.11-cpan-ceb78f64989 )