Initial Commit
This commit is contained in:
commit
77e8c510f4
5 changed files with 527 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
public/
|
||||||
|
*.lock
|
||||||
7
Gemfile
Normal file
7
Gemfile
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
source "https://rubygems.org"
|
||||||
|
|
||||||
|
gem "sinatra"
|
||||||
|
gem "sinatra-contrib"
|
||||||
|
gem "mini_magick"
|
||||||
|
gem "rackup"
|
||||||
|
gem "puma"
|
||||||
223
README.md
Normal file
223
README.md
Normal file
|
|
@ -0,0 +1,223 @@
|
||||||
|
# 📸 Rubymagick — Simple Ruby Image Converter & Pixelation Tool
|
||||||
|
|
||||||
|
**Rubymagick** is a tiny Ruby/Sinatra web app for converting images (like PNGs) into **JPEGs**, shrinking file sizes, or applying **fun retro effects** such as CRT scanlines and adjustable pixelation.
|
||||||
|
|
||||||
|
It’s designed to be lightweight, easy to run locally, and perfect for:
|
||||||
|
|
||||||
|
* Compressing PNGs into small JPGs
|
||||||
|
* Preparing images for websites or blog posts
|
||||||
|
* Pixelating art or photos (SNES / PS1 / Game Boy style)
|
||||||
|
* Adding fake CRT scanlines for retro aesthetics
|
||||||
|
* Quickly batch-converting images through a browser
|
||||||
|
|
||||||
|
No account, no cloud upload — just run it on your own machine.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ✨ Features
|
||||||
|
|
||||||
|
### ✔️ JPEG Conversion
|
||||||
|
|
||||||
|
Upload any image (PNG, JPG, WebP, HEIC, etc.) and convert it into a JPEG with adjustable quality.
|
||||||
|
|
||||||
|
### ✔️ Adjustable JPEG Quality Slider
|
||||||
|
|
||||||
|
Choose any value from **10–100**. Lower quality = smaller file size.
|
||||||
|
|
||||||
|
### ✔️ Pixelation Effect (with slider)
|
||||||
|
|
||||||
|
A fully adjustable pixelation slider:
|
||||||
|
|
||||||
|
* Factor **2** → subtle pixelation
|
||||||
|
* Factor **8** → chunky SNES/GBA style
|
||||||
|
* Factor **12–25** → extreme blocky PS1 / Minecraft look
|
||||||
|
|
||||||
|
### ✔️ CRT / Scanline Effect
|
||||||
|
|
||||||
|
Simulates a low-resolution CRT display:
|
||||||
|
|
||||||
|
* Downscales to fake 480p
|
||||||
|
* Adds contrast + noise
|
||||||
|
* Optionally overlays scanlines if `public/scanlines.png` exists
|
||||||
|
|
||||||
|
### ✔️ Local, Private, Simple
|
||||||
|
|
||||||
|
Everything runs on your machine.
|
||||||
|
Nothing leaves your computer.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🛠 Requirements
|
||||||
|
|
||||||
|
You need the following gems:
|
||||||
|
|
||||||
|
```
|
||||||
|
sinatra
|
||||||
|
sinatra-contrib
|
||||||
|
mini_magick
|
||||||
|
rackup
|
||||||
|
puma
|
||||||
|
```
|
||||||
|
|
||||||
|
Install them with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gem install sinatra sinatra-contrib mini_magick rackup puma
|
||||||
|
```
|
||||||
|
|
||||||
|
And you must have ImageMagick installed:
|
||||||
|
|
||||||
|
### Debian/Ubuntu
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
### Fedora/Rocky/RHEL
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo dnf install imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
### Arch
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo pacman -S imagemagick
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Running the App
|
||||||
|
|
||||||
|
Save the project as `app.rb`, then run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bundle exec ruby app.rb
|
||||||
|
```
|
||||||
|
|
||||||
|
Or if you're not using Bundler:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ruby app.rb
|
||||||
|
```
|
||||||
|
|
||||||
|
A provided `start-server.sh` Bash script is also available
|
||||||
|
|
||||||
|
Then visit:
|
||||||
|
|
||||||
|
```
|
||||||
|
http://localhost:4567/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🖼 How to Use
|
||||||
|
|
||||||
|
1. Open the web UI.
|
||||||
|
2. Upload an image (PNG, JPG, WEBP, etc.)
|
||||||
|
3. Choose your desired JPEG output quality.
|
||||||
|
4. Select an effect (None, CRT, or Pixelated).
|
||||||
|
5. If you choose Pixelated → adjust the intensity slider.
|
||||||
|
6. Click **Convert to .jpg**.
|
||||||
|
7. Download your JPEG file.
|
||||||
|
|
||||||
|
Converted files are stored in:
|
||||||
|
|
||||||
|
```
|
||||||
|
public/output/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🎛 Effects Explained
|
||||||
|
|
||||||
|
### 🔸 CRT / Scanline Mode
|
||||||
|
|
||||||
|
This effect simulates an old-school CRT monitor:
|
||||||
|
|
||||||
|
* Resizes image down to low resolution
|
||||||
|
* Boosts contrast
|
||||||
|
* Adds light noise for texture
|
||||||
|
* If `public/scanlines.png` exists, overlays it
|
||||||
|
|
||||||
|
Good for:
|
||||||
|
|
||||||
|
* Retro edits
|
||||||
|
* Vaporwave / synthwave style
|
||||||
|
* Terminal-style effects
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔸 Pixelation Mode (Adjustable)
|
||||||
|
|
||||||
|
Uses nearest-neighbor downscale → upscale trick.
|
||||||
|
|
||||||
|
Formula:
|
||||||
|
|
||||||
|
```
|
||||||
|
small_w = width / factor
|
||||||
|
small_h = height / factor
|
||||||
|
```
|
||||||
|
|
||||||
|
Then scales back up with no filtering.
|
||||||
|
|
||||||
|
Great for:
|
||||||
|
|
||||||
|
* Pixel art effects
|
||||||
|
* Lo-fi game textures
|
||||||
|
* Retro aesthetic
|
||||||
|
* Meme-style blur blocks
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📁 Optional: Add Custom Scanlines
|
||||||
|
|
||||||
|
Place a PNG named:
|
||||||
|
|
||||||
|
```
|
||||||
|
public/scanlines.png
|
||||||
|
```
|
||||||
|
|
||||||
|
It will be automatically applied when CRT mode is selected.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🧩 Project Structure
|
||||||
|
|
||||||
|
Typical layout:
|
||||||
|
|
||||||
|
```
|
||||||
|
rubymagick/
|
||||||
|
├─ app.rb
|
||||||
|
├─ Gemfile
|
||||||
|
├─ public/
|
||||||
|
│ ├─ output/
|
||||||
|
│ └─ scanlines.png (optional)
|
||||||
|
```
|
||||||
|
|
||||||
|
Output files go into `public/output/`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📝 About This App
|
||||||
|
|
||||||
|
Rubymagick is intentionally small and hackable.
|
||||||
|
It’s perfect as:
|
||||||
|
|
||||||
|
* A personal utility
|
||||||
|
* A learning project for Sinatra
|
||||||
|
* A way to explore ImageMagick through Ruby
|
||||||
|
* A lightweight image-prep tool for websites
|
||||||
|
|
||||||
|
You can expand it easily with:
|
||||||
|
|
||||||
|
* WebP/AVIF export
|
||||||
|
* Batch uploads
|
||||||
|
* Color palette reduction (NES/Game Boy)
|
||||||
|
* VHS distortion effects
|
||||||
|
* CLI wrapper mode
|
||||||
|
|
||||||
|
Just modify the helper functions or add more UI elements.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
292
app.rb
Normal file
292
app.rb
Normal file
|
|
@ -0,0 +1,292 @@
|
||||||
|
#!/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>
|
||||||
|
|
||||||
3
start-server.sh
Executable file
3
start-server.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
bundle3.3 exec ruby app.rb
|
||||||
Loading…
Add table
Add a link
Reference in a new issue