App-BlurFill
view release on metacpan or search on metacpan
lib/App/BlurFill/Web.pm view on Meta::CPAN
.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> /
Code <a href="https://github.com/davorg-cpan/app-blurfill">on GitHub</a>
</div>
</div>
</body>
</html>
HTML
};
post '/blur' => sub {
my $upload = upload('image')
or return status 400, { error => 'Missing image file' };
my $orig_name = $upload->filename;
my ($name, $path, $ext) =
File::Basename::fileparse($orig_name, qr/\.[^.]*$/);
return status 400, { error => 'Uploaded file must have a file extension' }
unless $ext;
my $format = lc $ext;
$format =~ s/^\.//;
my %mime = (
jpg => 'image/jpeg',
jpeg => 'image/jpeg',
png => 'image/png',
gif => 'image/gif',
);
return status 400, { error => "Unsupported file format: .$format" }
unless exists $mime{$format};
my $width = query_parameters->get('width') || 650;
my $height = query_parameters->get('height') || 350;
my $in_dir = File::Temp::tempdir;
my $in_path = "$in_dir/$name$ext";
$upload->copy_to($in_path);
my $outfile;
eval {
my $blur = App::BlurFill->new(
file => $in_path,
width => $width,
height => $height,
);
$outfile = $blur->process;
} or return status 500, { error => "Processing failed: $@" };
my ($out_name) = File::Basename::fileparse($outfile);
# Copy the processed file to our persistent temp directory
my $persistent_path = File::Spec->catfile($TEMP_DIR, $out_name);
File::Copy::copy($outfile, $persistent_path) or die "Copy failed: $!";
# Display results page with image preview and download link
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 - Result</title>
<style>
$css
</style>
</head>
<body>
<div class="container">
<h1>BlurFill</h1>
<p class="subtitle">Your resized image is ready!</p>
<div class="success-message">
<strong>â Success!</strong> Your image has been processed successfully.
</div>
<div class="result-image">
<img src="/download/$out_name" alt="Resized image preview">
</div>
<div class="action-buttons">
<a href="/download/$out_name" class="button" download>Download image</a>
<a href="/" class="button button-secondary">Create another</a>
</div>
<div class="info">
<p><strong>What's next?</strong></p>
<p>⢠Click "Download image" to save your resized background</p>
<p>⢠Click "Create another" to process a new image</p>
</div>
</div>
</body>
</html>
HTML
};
get '/download/:filename' => sub {
my $filename = route_parameters->get('filename');
# Security: only allow filenames without path traversal
return status 400, { error => 'Invalid filename' }
if $filename =~ m{[/\\]};
my $filepath = File::Spec->catfile($TEMP_DIR, $filename);
return status 404, { error => 'File not found' }
unless -f $filepath;
# Determine content type from extension
my $ext = lc($filename);
$ext =~ s/.*\.//;
my %mime = (
jpg => 'image/jpeg',
jpeg => 'image/jpeg',
png => 'image/png',
gif => 'image/gif',
);
my $content_type = $mime{$ext} || 'application/octet-stream';
( run in 1.390 second using v1.01-cache-2.11-cpan-39bf76dae61 )