Skip to content

Commit 88f1845

Browse files
committed
replace image engine to magick++
1 parent 4f874df commit 88f1845

File tree

7 files changed

+202
-159
lines changed

7 files changed

+202
-159
lines changed

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
tmp
2+
*.o
3+
*.sh
4+
*.txt

Dockerfile

+10-4
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,19 @@
1-
FROM golang:1.22.5 AS corebuilder
1+
FROM ubuntu:noble AS corebuilder
22
WORKDIR /work
33

4+
RUN apt update && apt install -y golang-go libmagick++-6.q16-dev
5+
46
COPY ./go.mod ./go.sum ./
57
RUN go mod download && go mod verify
68
COPY ./ ./
7-
RUN go build -o hyperproxy
9+
RUN CGO_CPPFLAGS="$(pkg-config --cflags Magick++)" \
10+
CGO_LDFLAGS="$(pkg-config --libs Magick++)" \
11+
go build -o hyperproxy
812

9-
FROM ubuntu:latest
10-
RUN apt-get update && apt-get install -y ca-certificates curl --no-install-recommends && rm -rf /var/lib/apt/lists/*
13+
FROM ubuntu:noble
14+
RUN apt-get update \
15+
&& apt-get install -y ca-certificates curl libmagickcore-6.q16-7t64 libmagick++-6.q16-9t64 libmagickwand-6.q16-7t64 ffmpeg --no-install-recommends \
16+
&& rm -rf /var/lib/apt/lists/*
1117

1218
COPY --from=corebuilder /work/hyperproxy /usr/local/bin
1319

image.go

+75-155
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package main
22

33
import (
44
"bufio"
5-
"bytes"
65
"crypto/sha256"
76
"encoding/hex"
87
"fmt"
@@ -17,20 +16,8 @@ import (
1716
"strconv"
1817
"strings"
1918

20-
_ "github.com/jdeng/goheif"
21-
"github.com/kettek/apng"
22-
_ "golang.org/x/image/bmp"
23-
_ "golang.org/x/image/tiff"
24-
_ "golang.org/x/image/webp"
25-
"image"
26-
"image/gif"
27-
_ "image/jpeg"
28-
29-
"github.com/chai2010/webp"
30-
"github.com/disintegration/imaging"
3119
"github.com/labstack/echo/v4"
3220
"github.com/pkg/errors"
33-
"github.com/rwcarlsen/goexif/exif"
3421
"go.opentelemetry.io/otel/attribute"
3522
)
3623

@@ -125,6 +112,7 @@ func ImageHandler(c echo.Context) error {
125112
span.RecordError(err)
126113
return c.String(400, err.Error())
127114
}
115+
128116
widthStr := split[0]
129117
heightStr := split[1]
130118

@@ -154,40 +142,42 @@ func ImageHandler(c echo.Context) error {
154142
remoteURL = reCleanedURL.ReplaceAllString(remoteURL, "$1://$2")
155143
span.SetAttributes(attribute.String("remoteURL", remoteURL))
156144

157-
requestCacheKeyBytes := sha256.Sum256([]byte(remoteURL))
158-
requestCacheKey := hex.EncodeToString(requestCacheKeyBytes[:])
159-
requestCachePath := filepath.Join(CachePath, requestCacheKey)
145+
fmt.Println("Request:", remoteURL, width, height)
160146

161-
var reader io.Reader
162-
var contentType string
147+
originalCacheKeyBytes := sha256.Sum256([]byte(remoteURL))
148+
originalCacheKey := hex.EncodeToString(originalCacheKeyBytes[:])
149+
originalCachePath := filepath.Join(CachePath, originalCacheKey)
163150

164-
// Check if the original image is already cached
165-
if _, err := os.Stat(requestCachePath); err == nil {
166-
167-
fmt.Println("Cache hit: ", remoteURL)
151+
requestCacheKeyBytes := sha256.Sum256([]byte(c.Request().RequestURI))
152+
requestCacheKey := hex.EncodeToString(requestCacheKeyBytes[:])
153+
requestCachePath := filepath.Join(CachePath, requestCacheKey)
168154

169-
cache, err := os.Open(requestCachePath)
170-
if err != nil {
171-
err := errors.Wrap(err, "Failed to open original cache")
172-
span.RecordError(err)
173-
return c.String(500, err.Error())
174-
}
155+
// check cache
156+
if _, err := os.Stat(requestCachePath + ".data"); err == nil {
157+
fmt.Println(" Cache hit")
158+
c.Response().Header().Set("Content-Type", "image/webp")
159+
c.Response().Header().Set("Cache-Control", "public, max-age=86400, s-maxage=86400, immutable")
160+
return c.File(requestCachePath + ".data")
161+
}
175162

176-
req := &http.Request{}
177-
resp, err := http.ReadResponse(bufio.NewReader(cache), req)
178-
if err != nil {
179-
err := errors.Wrap(err, "Failed to read response")
180-
span.RecordError(err)
181-
return c.String(500, err.Error())
182-
}
163+
// check if the original image is already cached
164+
data_cached := false
165+
header_cached := false
183166

184-
reader = resp.Body
185-
contentType = resp.Header.Get("Content-Type")
167+
if _, err := os.Stat(originalCachePath + ".data"); err == nil {
168+
data_cached = true
169+
}
186170

187-
} else {
171+
header, err := os.Open(originalCachePath + ".header")
172+
if err == nil {
173+
header_cached = true
174+
}
188175

189-
fmt.Println("Cache miss: ", remoteURL)
176+
resp := &http.Response{}
190177

178+
if !data_cached || !header_cached {
179+
fmt.Println(" Fetch Original Image")
180+
191181
parsedUrl, err := url.Parse(remoteURL)
192182
if err != nil {
193183
err := errors.Wrap(err, "Failed to parse URL")
@@ -237,47 +227,36 @@ func ImageHandler(c echo.Context) error {
237227
req, err := http.NewRequest("GET", remoteURL, nil)
238228
if err != nil {
239229
err := errors.Wrap(err, "Failed to create request")
240-
span.RecordError(err)
230+
fetchSpan.RecordError(err)
241231
return c.String(500, err.Error())
242232
}
243233
req.Header.Set("User-Agent", useragent)
244-
resp, err := client.Do(req)
234+
resp, err = client.Do(req)
245235
if err != nil {
246236
err := errors.Wrap(err, "Failed to fetch image")
247-
span.RecordError(err)
237+
fetchSpan.RecordError(err)
248238
return c.String(500, err.Error())
249239
}
250240
defer resp.Body.Close()
251241

252-
buf, err := io.ReadAll(resp.Body)
253-
if err != nil {
254-
err := errors.Wrap(err, "Failed to read response")
255-
span.RecordError(err)
256-
return c.String(500, err.Error())
257-
}
258-
259-
resp.Body = io.NopCloser(bytes.NewReader(buf))
260-
reader = bytes.NewReader(buf)
261-
262-
contentType = resp.Header.Get("Content-Type")
263-
264-
fetchSpan.End()
242+
contentType := resp.Header.Get("Content-Type")
265243

266244
if resp.StatusCode != 200 {
267245
err := errors.New("fetch image response code is not 200")
268-
span.SetAttributes(attribute.Int("statusCode", resp.StatusCode))
269-
span.SetAttributes(attribute.String("body", string(buf)))
270-
span.RecordError(err)
246+
fetchSpan.SetAttributes(attribute.Int("statusCode", resp.StatusCode))
247+
fetchSpan.RecordError(err)
271248
return c.String(resp.StatusCode, err.Error())
272249
}
273250

274251
// check if the image is valid
275-
if !strings.HasPrefix(resp.Header.Get("Content-Type"), "image/") {
252+
if !strings.HasPrefix(contentType, "image/") {
276253
err := errors.New("Invalid image")
277-
span.RecordError(err)
254+
fetchSpan.RecordError(err)
278255
return c.String(400, err.Error())
279256
}
280257

258+
fetchSpan.End()
259+
281260
// save the image to cache
282261
err = os.MkdirAll(CachePath, 0755)
283262
if err != nil {
@@ -286,119 +265,60 @@ func ImageHandler(c echo.Context) error {
286265
return c.String(500, err.Error())
287266
}
288267

289-
cache, err := os.Create(requestCachePath)
268+
dataCachePath := originalCachePath + ".data"
269+
cache, err := os.Create(dataCachePath)
290270
if err != nil {
291271
err := errors.Wrap(err, "Failed to create cache file")
292272
span.RecordError(err)
293273
return c.String(500, err.Error())
294274
}
295275
defer cache.Close()
296-
resp.Write(cache)
297-
}
276+
io.Copy(cache, resp.Body)
298277

299-
// load image
300-
_, loadSpan := tracer.Start(ctx, "LoadImage")
301-
data, err := io.ReadAll(reader)
302-
img, format, err := image.Decode(bytes.NewReader(data))
303-
304-
// check if the image is animated
305-
isAnimated := false
306-
if err == nil {
307-
switch format {
308-
case "gif":
309-
gifImg, err := gif.DecodeAll(bytes.NewReader(data))
310-
if err == nil && len(gifImg.Image) > 1 {
311-
isAnimated = true
312-
}
313-
case "apng":
314-
apngImg, err := apng.DecodeAll(bytes.NewReader(data))
315-
if err == nil && len(apngImg.Frames) > 1 {
316-
isAnimated = true
317-
}
318-
}
319-
}
320-
321-
if err != nil || isAnimated {
278+
headerCachePath := originalCachePath + ".header"
279+
cache, err = os.Create(headerCachePath)
322280
if err != nil {
323-
fmt.Printf("Fallback to original image: %s (%s) %s\n", remoteURL, format, err)
281+
err := errors.Wrap(err, "Failed to create cache file")
282+
span.RecordError(err)
283+
return c.String(500, err.Error())
324284
}
325-
c.Response().Header().Set("Cache-Control", "public, max-age=86400, s-maxage=86400, immutable")
326-
return c.Stream(200, contentType, bytes.NewReader(data))
327-
}
328-
loadSpan.End()
329-
330-
orientation := 1
331-
if format == "jpeg" {
332-
exifData, err := exif.Decode(bytes.NewReader(data))
333-
if err == nil {
334-
exifOrient, err := exifData.Get(exif.Orientation)
335-
if err == nil {
336-
orientation, err = exifOrient.Int(0)
337-
if err != nil {
338-
fmt.Println("Error parsing orientation: ", err)
339-
}
340-
}
285+
defer cache.Close()
286+
resp.Write(cache)
287+
} else {
288+
fmt.Println(" Original Image Cache found")
289+
var err error
290+
resp, err = http.ReadResponse(bufio.NewReader(header), nil)
291+
if err != nil {
292+
err := errors.Wrap(err, "Failed to read response")
293+
span.RecordError(err)
294+
return c.String(500, err.Error())
341295
}
342296
}
343297

344-
originalWidth := img.Bounds().Dx()
345-
originalHeight := img.Bounds().Dy()
346-
347-
if orientation >= 5 {
348-
originalWidth, originalHeight = originalHeight, originalWidth
349-
}
350-
351-
resizeWidth := width
352-
resizeHeight := height
353-
354-
if resizeWidth > originalWidth {
355-
resizeWidth = originalWidth
356-
}
357-
358-
if resizeHeight > originalHeight {
359-
resizeHeight = originalHeight
298+
prefix := ""
299+
if strings.HasSuffix(remoteURL, ".apng") {
300+
prefix = "apng:"
360301
}
361302

362-
// resize image
363-
_, resizeSpan := tracer.Start(ctx, "ResizeImage")
364-
365-
switch orientation {
366-
case 2:
367-
img = imaging.FlipH(img)
368-
case 3:
369-
img = imaging.Rotate180(img)
370-
case 4:
371-
img = imaging.FlipV(img)
372-
case 5:
373-
img = imaging.Transpose(img)
374-
case 6:
375-
img = imaging.Rotate270(img)
376-
case 7:
377-
img = imaging.Transverse(img)
378-
case 8:
379-
img = imaging.Rotate90(img)
380-
}
381-
382-
if (resizeWidth == 0 || resizeWidth == originalWidth) && (resizeHeight == 0 || resizeHeight == originalHeight) {
383-
// no need to resize
384-
} else {
385-
img = imaging.Resize(img, resizeWidth, resizeHeight, imaging.CatmullRom)
303+
if width == 0 && height == 0 {
304+
fmt.Println(" Returning original image")
305+
c.Response().Header().Set("Cache-Control", "public, max-age=86400, s-maxage=86400, immutable")
306+
c.Response().Header().Set("Content-Type", resp.Header.Get("Content-Type"))
307+
return c.File(originalCachePath + ".data")
386308
}
387309

388-
resizeSpan.End()
389-
390-
// encode image
391-
_, encodeSpan := tracer.Start(ctx, "EncodeImage")
392-
var buff bytes.Buffer
393-
err = webp.Encode(&buff, img, &webp.Options{Quality: 80})
394-
if err != nil {
395-
err := errors.Wrap(err, "Failed to encode image")
310+
ok := resize(prefix + originalCachePath + ".data", requestCachePath + ".data", width, height)
311+
if ok != 0 {
312+
fmt.Println(" [error] Resize Fail Returning original image")
313+
err := errors.New("Failed to resize image")
396314
span.RecordError(err)
397-
return c.String(500, err.Error())
315+
c.Response().Header().Set("Cache-Control", "public, max-age=86400, s-maxage=86400, immutable")
316+
c.Response().Header().Set("Content-Type", resp.Header.Get("Content-Type"))
317+
return c.File(originalCachePath + ".data")
398318
}
399-
encodeSpan.End()
400319

401-
// return the image
320+
fmt.Println(" Returning resized image")
321+
c.Response().Header().Set("Content-Type", "image/webp")
402322
c.Response().Header().Set("Cache-Control", "public, max-age=86400, s-maxage=86400, immutable")
403-
return c.Stream(200, "image/webp", &buff)
323+
return c.File(requestCachePath + ".data")
404324
}

main.go

+2
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,8 @@ func main() {
7878
},
7979
}))
8080

81+
init_resize(512 * 1024 * 1024)
82+
8183
e.GET("/image/*", ImageHandler)
8284
e.GET("/summary", SummaryHandler)
8385

0 commit comments

Comments
 (0)