rubymagick/app.rb
2025-11-21 02:56:46 +00:00

292 lines
7.2 KiB
Ruby

#!/usr/bin/env ruby
# app.rb — Simple Ruby/Sinatra Image → JPEG converter
# Supports:
# - JPEG quality slider
# - CRT/scanline retro effect
# - Pixelation (with user slider)
#
# Requires:
# gem install sinatra sinatra-contrib mini_magick rackup puma
# sudo apt install imagemagick
#
# Run:
# bundle exec ruby app.rb
#
# Then open: http://localhost:4567/
require "sinatra"
require "sinatra/reloader" if development?
require "mini_magick"
require "fileutils"
require "securerandom"
set :bind, "0.0.0.0"
set :port, 4567
set :public_folder, File.join(__dir__, "public")
OUTPUT_DIR = File.join(settings.public_folder, "output")
FileUtils.mkdir_p OUTPUT_DIR
# -------------------------------------------------------------------
# HELPERS — Image effects + utilities
# -------------------------------------------------------------------
helpers do
# -----------------------------------------------------------------
# CRT / Scanline effect
# -----------------------------------------------------------------
def apply_crt_effect(image)
image.combine_options do |c|
c.resize "640x480"
c.contrast
c.noise "2"
end
scanlines_path = File.join(settings.public_folder, "scanlines.png")
if File.exist?(scanlines_path)
scan = MiniMagick::Image.open(scanlines_path)
scan.resize "#{image.width}x#{image.height}!"
image = image.composite(scan) do |c|
c.compose "overlay"
c.gravity "center"
end
end
image
end
# -----------------------------------------------------------------
# Pixelation effect with user-adjustable factor
# -----------------------------------------------------------------
def apply_pixelate_effect(image, factor = 8)
width = image.width
height = image.height
small_w = [width / factor, 1].max
small_h = [height / factor, 1].max
image.combine_options do |c|
c.filter "point"
c.resize "#{small_w}x#{small_h}!"
c.resize "#{width}x#{height}!"
end
image
end
# Human-readable formatting
def human_size(bytes)
kb = bytes / 1024.0
return "#{kb.round(1)} KB" if kb < 1024
mb = kb / 1024.0
"#{mb.round(2)} MB"
end
end
# -------------------------------------------------------------------
# ROUTES
# -------------------------------------------------------------------
get "/" do
erb :index
end
post "/convert" do
unless params[:image] && params[:image][:tempfile]
@error = "Please choose an image file."
return erb :index
end
tempfile = params[:image][:tempfile]
filename = params[:image][:filename]
quality = (params[:quality] || "80").to_i
effect = params[:effect]
quality = 1 if quality < 1
quality = 100 if quality > 100
begin
image = MiniMagick::Image.open(tempfile.path)
# ------------------------------
# Apply effects
# ------------------------------
case effect
when "crt"
image = apply_crt_effect(image)
@effect_used = "CRT / scanline"
when "pixel"
factor = (params[:pixel_factor] || 8).to_i
factor = 2 if factor < 2
factor = 25 if factor > 25
image = apply_pixelate_effect(image, factor)
@effect_used = "Pixelated (factor #{factor})"
else
@effect_used = "None"
end
# Convert to JPEG
image.format "jpg"
image.quality quality.to_s
output_name = "converted-#{SecureRandom.hex(8)}.jpg"
output_path = File.join(OUTPUT_DIR, output_name)
image.write(output_path)
@original_name = filename
@output_url = "/output/#{output_name}"
@output_size = human_size(File.size(output_path))
@chosen_quality = quality
erb :result
rescue => e
@error = "Conversion failed: #{e.message}"
erb :index
end
end
# -------------------------------------------------------------------
# INLINE HTML TEMPLATES
# -------------------------------------------------------------------
__END__
@@layout
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Rubymagick JPG Converter</title>
<style>
body {
background: #111;
color: #eee;
font-family: system-ui, sans-serif;
max-width: 800px;
margin: 2rem auto;
padding: 0 1rem;
}
h1, h2 { color: #6cf; }
.card {
background: #1b1b1b;
border-radius: 8px;
padding: 1.5rem;
box-shadow: 0 0 15px rgba(0,0,0,0.6);
border: 1px solid #333;
}
label {
display: block;
margin-top: 1rem;
margin-bottom: 0.25rem;
font-weight: 600;
}
input[type="file"],
select,
button,
input[type="range"] {
width: 100%;
padding: 0.5rem;
border-radius: 4px;
border: 1px solid #333;
background: #222;
color: #eee;
}
button {
margin-top: 1.5rem;
background: #0a84ff;
cursor: pointer;
border: none;
font-weight: 600;
}
button:hover { background: #0062cc; }
img.preview {
max-width: 100%;
border-radius: 8px;
margin-top: 1rem;
border: 1px solid #333;
}
</style>
</head>
<body>
<h1>♦️ Image → JPEG Converter</h1>
<%= yield %>
</body>
</html>
@@index
<div class="card">
<% if @error %><div class="error"><%= @error %></div><% end %>
<form action="/convert" method="post" enctype="multipart/form-data">
<label for="image">Image file</label>
<input type="file" id="image" name="image" accept="image/*" required>
<label for="quality">JPEG quality:
<span id="qualityValue">80</span>
</label>
<input
type="range"
id="quality"
name="quality"
min="10"
max="100"
value="80"
oninput="document.getElementById('qualityValue').textContent = this.value"
>
<label for="effect">Effect</label>
<select id="effect" name="effect">
<option value="none">None</option>
<option value="crt">CRT / Scanline</option>
<option value="pixel">Pixelated</option>
</select>
<!-- Pixelation slider (hidden until selected) -->
<div id="pixelControls" style="display:none;">
<label for="pixel_factor">Pixelation intensity:
<span id="pixelFactorValue">8</span>
</label>
<input
type="range"
id="pixel_factor"
name="pixel_factor"
min="2"
max="25"
value="8"
oninput="document.getElementById('pixelFactorValue').textContent = this.value"
>
</div>
<script>
const effectSelect = document.getElementById("effect");
const pixelControls = document.getElementById("pixelControls");
effectSelect.addEventListener("change", () => {
pixelControls.style.display =
effectSelect.value === "pixel" ? "block" : "none";
});
</script>
<button type="submit">Convert to .jpg</button>
</form>
</div>
@@result
<div class="card">
<h2>Conversion complete</h2>
<p>
Original: <strong><%= @original_name %></strong><br>
Quality: <strong><%= @chosen_quality %></strong><br>
Effect: <strong><%= @effect_used %></strong><br>
Output size: <strong><%= @output_size %></strong>
</p>
<img class="preview" src="<%= @output_url %>" alt="Converted image preview">
<br>
<a class="download" href="<%= @output_url %>" download>Download JPEG</a>
</div>