Skip to content

Commit 311df38

Browse files
committed
initial commit
0 parents  commit 311df38

File tree

9 files changed

+364
-0
lines changed

9 files changed

+364
-0
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
main

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
run:
2+
go run *.go -o ./tmp
3+
build:
4+
go build -o image-compressor

README.md

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# Image Compressor API
2+
3+
## Overview
4+
5+
Image Compressor API is a simple HTTP service written in Go that allows you to compress and resize images from a given URL. It supports popular image formats such as JPEG, PNG, and WebP.
6+
7+
## Features
8+
9+
- Image compression and resizing based on provided parameters.
10+
- Automatic determination of the image format based on the URL's content type.
11+
- Support for JPEG, PNG, and WebP output formats.
12+
- Option to specify output quality and resolution.
13+
- Efficient caching: If the compressed image already exists, it is served without re-compression.
14+
15+
## Getting Started
16+
17+
### Prerequisites
18+
19+
- Go (Golang) installed on your machine.
20+
- [mux](https://github.com/gorilla/mux), [nfnt/resize](https://github.com/nfnt/resize), and [chai2010/webp](https://github.com/chai2010/webp) Go packages.
21+
22+
### Installation
23+
24+
1. Clone the repository:
25+
26+
```bash
27+
git clone https://github.com/yourusername/your-repo.git
28+
cd your-repo
29+
```
30+
31+
2. Install dependencies:
32+
33+
```bash
34+
go get -u github.com/gorilla/mux
35+
go get -u github.com/nfnt/resize
36+
go get -u github.com/chai2010/webp
37+
```
38+
39+
3. Build and run the project:
40+
41+
```bash
42+
go run *.go -o ./tmp
43+
```
44+
45+
Alternatively, for a production build:
46+
47+
```bash
48+
go build -o image-compressor
49+
./image-compressor -o ./tmp
50+
```
51+
52+
### Usage
53+
54+
To compress an image, make a GET request to the `/compressor` endpoint with the following parameters:
55+
56+
- `url`: URL of the image to be compressed.
57+
- `output`: Desired output format (e.g., "jpeg", "png", "webp").
58+
- `quality`: Output quality (0-100, applicable for JPEG).
59+
- `resolution`: Output resolution in the format "widthxheight" (e.g., "1024x720").
60+
61+
Example:
62+
63+
```bash
64+
curl "http://localhost:8080/compressor?url=https://example.com/image.jpg&output=webp&quality=80&resolution=1024x720"
65+
```
66+
67+
## API Endpoints
68+
69+
### `/compressor`
70+
71+
- **Method:** GET
72+
- **Parameters:**
73+
- `url` (required): URL of the image to be compressed.
74+
- `output` (optional): Desired output format (e.g., "jpeg", "png", "webp").
75+
- `quality` (optional): Output quality (0-100, applicable for JPEG).
76+
- `resolution` (optional): Output resolution in the format "widthxheight" (e.g., "1024x720").
77+
78+
Example:
79+
80+
```bash
81+
curl "http://localhost:8080/compressor?url=https://example.com/image.jpg&output=webp&quality=80&resolution=1024x720"
82+
```
83+
84+
## License
85+
86+
This project is licensed under the [MIT License](LICENSE).

go.mod

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
module github.com/daniwebdev/image-compressor-api
2+
3+
go 1.20
4+
5+
require (
6+
github.com/chai2010/webp v1.1.1 // indirect
7+
github.com/gorilla/mux v1.8.1 // indirect
8+
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
9+
)

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
github.com/chai2010/webp v1.1.1 h1:jTRmEccAJ4MGrhFOrPMpNGIJ/eybIgwKpcACsrTEapk=
2+
github.com/chai2010/webp v1.1.1/go.mod h1:0XVwvZWdjjdxpUEIf7b9g9VkHFnInUSYujwqTLEuldU=
3+
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
4+
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
5+
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
6+
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=

image-compressor

8.75 MB
Binary file not shown.

main.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package main
2+
3+
import (
4+
"crypto/md5"
5+
"encoding/hex"
6+
"flag"
7+
"fmt"
8+
"image"
9+
"image/jpeg"
10+
"image/png"
11+
"io"
12+
"net/http"
13+
"os"
14+
"path/filepath"
15+
"strings"
16+
17+
"github.com/chai2010/webp"
18+
"github.com/gorilla/mux"
19+
"github.com/nfnt/resize"
20+
)
21+
22+
var outputDirectory string
23+
24+
func init() {
25+
flag.StringVar(&outputDirectory, "o", ".", "Output directory for compressed images")
26+
flag.Parse()
27+
}
28+
29+
func downloadImage(url string) (image.Image, string, error) {
30+
resp, err := http.Get(url)
31+
if err != nil {
32+
return nil, "", err
33+
}
34+
defer resp.Body.Close()
35+
36+
var img image.Image
37+
var format string
38+
39+
// Determine the image format based on content type
40+
contentType := resp.Header.Get("Content-Type")
41+
switch {
42+
case strings.Contains(contentType, "jpeg"):
43+
img, _, err = image.Decode(resp.Body)
44+
format = "jpeg"
45+
case strings.Contains(contentType, "png"):
46+
img, _, err = image.Decode(resp.Body)
47+
format = "png"
48+
case strings.Contains(contentType, "webp"):
49+
img, err = webp.Decode(resp.Body)
50+
format = "webp"
51+
default:
52+
return nil, "", fmt.Errorf("unsupported image format")
53+
}
54+
55+
if err != nil {
56+
return nil, "", err
57+
}
58+
59+
return img, format, nil
60+
}
61+
62+
func compressImage(img image.Image, format, output string, quality int, resolution string) error {
63+
// Resize the image if resolution is provided
64+
if resolution != "" {
65+
size := strings.Split(resolution, "x")
66+
width, height := parseResolution(size[0], size[1], img.Bounds().Dx(), img.Bounds().Dy())
67+
img = resize.Resize(width, height, img, resize.Lanczos3)
68+
}
69+
70+
// Create the output file in the specified directory
71+
out, err := os.Create(filepath.Join(outputDirectory, output))
72+
if err != nil {
73+
return err
74+
}
75+
defer out.Close()
76+
77+
// Compress and save the image in the specified format
78+
switch format {
79+
case "jpeg":
80+
options := jpeg.Options{Quality: quality}
81+
err = jpeg.Encode(out, img, &options)
82+
case "png":
83+
encoder := png.Encoder{CompressionLevel: png.BestCompression}
84+
err = encoder.Encode(out, img)
85+
case "webp":
86+
options := &webp.Options{Lossless: true}
87+
err = webp.Encode(out, img, options)
88+
default:
89+
return fmt.Errorf("unsupported output format")
90+
}
91+
92+
if err != nil {
93+
return err
94+
}
95+
96+
return nil
97+
}
98+
99+
func generateMD5Hash(input string) string {
100+
hasher := md5.New()
101+
hasher.Write([]byte(input))
102+
return hex.EncodeToString(hasher.Sum(nil))
103+
}
104+
105+
func atoi(s string) int {
106+
result := 0
107+
for _, c := range s {
108+
result = result*10 + int(c-'0')
109+
}
110+
return result
111+
}
112+
113+
func parseResolution(width, height string, originalWidth, originalHeight int) (uint, uint) {
114+
var newWidth, newHeight uint
115+
116+
if width == "auto" && height == "auto" {
117+
// If both dimensions are "auto," maintain the original size
118+
newWidth = uint(originalWidth)
119+
newHeight = uint(originalHeight)
120+
} else if width == "auto" {
121+
// If width is "auto," calculate height maintaining the aspect ratio
122+
ratio := float64(originalWidth) / float64(originalHeight)
123+
newHeight = uint(atoi(height))
124+
newWidth = uint(float64(newHeight) * ratio)
125+
} else if height == "auto" {
126+
// If height is "auto," calculate width maintaining the aspect ratio
127+
ratio := float64(originalHeight) / float64(originalWidth)
128+
newWidth = uint(atoi(width))
129+
newHeight = uint(float64(newWidth) * ratio)
130+
} else {
131+
// Use the provided width and height
132+
newWidth = uint(atoi(width))
133+
newHeight = uint(atoi(height))
134+
}
135+
136+
return newWidth, newHeight
137+
}
138+
139+
func compressHandler(w http.ResponseWriter, r *http.Request) {
140+
url := r.URL.Query().Get("url")
141+
format := r.URL.Query().Get("output")
142+
quality := r.URL.Query().Get("quality")
143+
resolution := r.URL.Query().Get("resolution")
144+
145+
// Concatenate parameters into a single string
146+
paramsString := fmt.Sprintf("%s-%s-%s-%s", url, format, quality, resolution)
147+
148+
// Generate MD5 hash from the concatenated parameters
149+
hash := generateMD5Hash(paramsString)
150+
151+
// Generate the output filename using the hash and format
152+
output := fmt.Sprintf("%s.%s", hash, format)
153+
154+
// Check if the compressed file already exists in the output directory
155+
filePath := filepath.Join(outputDirectory, output)
156+
if _, err := os.Stat(filePath); err == nil {
157+
// File exists, no need to download and compress again
158+
159+
// Open and send the existing compressed image file
160+
compressedFile, err := os.Open(filePath)
161+
if err != nil {
162+
http.Error(w, fmt.Sprintf("Error opening compressed image file: %s", err), http.StatusInternalServerError)
163+
return
164+
}
165+
defer compressedFile.Close()
166+
167+
// Set the appropriate Content-Type based on the output format
168+
var contentType string
169+
switch format {
170+
case "jpeg":
171+
contentType = "image/jpeg"
172+
case "png":
173+
contentType = "image/png"
174+
case "webp":
175+
contentType = "image/webp"
176+
default:
177+
http.Error(w, "Unsupported output format", http.StatusInternalServerError)
178+
return
179+
}
180+
181+
// Set the Content-Type header
182+
w.Header().Set("Content-Type", contentType)
183+
184+
// Copy the existing compressed image file to the response writer
185+
_, err = io.Copy(w, compressedFile)
186+
if err != nil {
187+
http.Error(w, fmt.Sprintf("Error sending compressed image: %s", err), http.StatusInternalServerError)
188+
return
189+
}
190+
191+
return
192+
}
193+
194+
img, imgFormat, err := downloadImage(url)
195+
if err != nil {
196+
http.Error(w, fmt.Sprintf("Error downloading image: %s", err), http.StatusInternalServerError)
197+
return
198+
}
199+
200+
if format == "" {
201+
format = imgFormat
202+
}
203+
204+
err = compressImage(img, format, output, atoi(quality), resolution)
205+
if err != nil {
206+
http.Error(w, fmt.Sprintf("Error compressing image: %s", err), http.StatusInternalServerError)
207+
return
208+
}
209+
210+
// Set the appropriate Content-Type based on the output format
211+
var contentType string
212+
switch format {
213+
case "jpeg":
214+
contentType = "image/jpeg"
215+
case "png":
216+
contentType = "image/png"
217+
case "webp":
218+
contentType = "image/webp"
219+
default:
220+
http.Error(w, "Unsupported output format", http.StatusInternalServerError)
221+
return
222+
}
223+
224+
// Set the Content-Type header
225+
w.Header().Set("Content-Type", contentType)
226+
227+
// Open and send the compressed image file
228+
compressedFile, err := os.Open(filePath)
229+
if err != nil {
230+
http.Error(w, fmt.Sprintf("Error opening compressed image file: %s", err), http.StatusInternalServerError)
231+
return
232+
}
233+
defer compressedFile.Close()
234+
235+
// Copy the compressed image file to the response writer
236+
_, err = io.Copy(w, compressedFile)
237+
if err != nil {
238+
http.Error(w, fmt.Sprintf("Error sending compressed image: %s", err), http.StatusInternalServerError)
239+
return
240+
}
241+
fmt.Fprintf(w, "Image compressed and saved to %s\n", filePath)
242+
}
243+
244+
245+
246+
func main() {
247+
r := mux.NewRouter()
248+
r.HandleFunc("/compressor", compressHandler).Methods("GET")
249+
r.HandleFunc("/compressor/{filename}", compressHandler).Methods("GET")
250+
251+
http.Handle("/", r)
252+
253+
fmt.Printf("Server is listening on :8080. Output directory: %s\n", outputDirectory)
254+
http.ListenAndServe(":8080", nil)
255+
}

tmp/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*
2+
!README.md
3+
!.gitignore

tmp/README.md

Whitespace-only changes.

0 commit comments

Comments
 (0)