diff --git a/.gitignore b/.gitignore index 4346d5327e01732cf9652dc050b290cbf10e39da..f7f8ac3430768cab865f28157f46200a73829459 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,2 @@ /.idea/ /vendor/ -/node_modules/ -/cli/ -/assets/css -/imghost diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8e5a8d819613e574c677b99f3c5724cad218b338..7ffe89bb7569426c8d516db9563a34b28c22e270 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -16,3 +16,12 @@ frontend: - mkdir -p /kaniko/.docker - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.frontend --destination $CI_REGISTRY_IMAGE:frontend-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} --destination $CI_REGISTRY_IMAGE:frontend +cli: + stage: build + image: + name: gcr.io/kaniko-project/executor:debug + entrypoint: [ "" ] + script: + - mkdir -p /kaniko/.docker + - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /kaniko/.docker/config.json + - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile.cli --destination $CI_REGISTRY_IMAGE:cli-${CI_COMMIT_TAG:-$CI_COMMIT_SHORT_SHA} --destination $CI_REGISTRY_IMAGE:cli diff --git a/Dockerfile.backend b/Dockerfile.backend index 55b1bb0448e1ced4e40e002be1bc72293262d038..fdd3524177864eac8665624bf605860ee85c301b 100644 --- a/Dockerfile.backend +++ b/Dockerfile.backend @@ -1,24 +1,19 @@ FROM golang:1.17-alpine3.15 AS go_builder RUN apk --no-cache add \ - --virtual .build-deps \ - alpine-sdk \ - cmake \ - sudo \ - libssh2 libssh2-dev \ - git \ - dep \ - bash \ - curl \ - imagemagick \ - imagemagick-dev + build-base \ + imagemagick6 \ + imagemagick6-dev WORKDIR /go/src/app COPY go.* ./ +ENV CGO_ENABLED=1 +ENV CGO_CFLAGS_ALLOW=-Xpreprocessor +ENV GOPROXY=https://proxy.golang.org RUN go mod download COPY . ./ RUN go build -o app ./backend FROM alpine:3.15 -RUN apk --no-cache add imagemagick +RUN apk --no-cache add imagemagick6 RUN addgroup -g 1000 -S app && \ adduser -u 1000 -G app -S app COPY --from=go_builder /go/src/app / diff --git a/Dockerfile.frontend b/Dockerfile.frontend index e9356ffa1d26bdc283e9fce213143e65cd186571..f0368f99155e6403b17b0ab4e260ced958c0eaec 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -1,32 +1,20 @@ FROM golang:1.17-alpine3.15 AS go_builder RUN apk --no-cache add \ - --virtual .build-deps \ - alpine-sdk \ - cmake \ - sudo \ - libssh2 libssh2-dev \ - git \ - dep \ - bash \ - curl \ - imagemagick \ - imagemagick-dev + build-base \ + imagemagick6 \ + imagemagick6-dev WORKDIR /go/src/app COPY go.* ./ +ENV CGO_ENABLED=1 +ENV CGO_CFLAGS_ALLOW=-Xpreprocessor +ENV GOPROXY=https://proxy.golang.org RUN go mod download COPY . ./ RUN go build -o app ./frontend FROM node:alpine as asset_builder RUN apk --no-cache add \ - --virtual .build-deps \ - alpine-sdk \ - cmake \ - libssh2 libssh2-dev \ - git \ - dep \ - bash \ - curl \ + build-base \ python3 WORKDIR /app COPY frontend/package* /app/ @@ -35,10 +23,10 @@ COPY frontend/assets /app/assets RUN npm run build FROM alpine:3.15 -RUN apk --no-cache add imagemagick +RUN apk --no-cache add imagemagick6 RUN addgroup -g 1000 -S app && \ adduser -u 1000 -G app -S app -COPY --from=go_builder /go/src/app/app / +COPY --from=go_builder /go/src/app / COPY frontend/templates /templates COPY --from=asset_builder /app/assets /assets USER app diff --git a/Dockerfile.imgconv b/Dockerfile.imgconv new file mode 100644 index 0000000000000000000000000000000000000000..a89fee84486a05b7acc20cf1a4323d6a4d0b0fb4 --- /dev/null +++ b/Dockerfile.imgconv @@ -0,0 +1,21 @@ +FROM golang:1.17-alpine3.15 AS go_builder +RUN apk --no-cache add \ + build-base \ + imagemagick6 \ + imagemagick6-dev +WORKDIR /go/src/app +COPY go.* ./ +ENV CGO_ENABLED=1 +ENV CGO_CFLAGS_ALLOW=-Xpreprocessor +ENV GOPROXY=https://proxy.golang.org +RUN go mod download +COPY . ./ +RUN go build -o app ./cli + +FROM alpine:3.15 +RUN apk --no-cache add imagemagick6 +RUN addgroup -g 1000 -S app && \ + adduser -u 1000 -G app -S app +COPY --from=go_builder /go/src/app / +USER app +ENTRYPOINT ["/app"] diff --git a/backend/main.go b/backend/main.go index 0b3dd2bac5870e20a5a2d9a5e4c3fafc79bc34ca..16ff28dc911930b5cd0ebb14de155f832bcdc128 100644 --- a/backend/main.go +++ b/backend/main.go @@ -7,7 +7,7 @@ import ( "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promhttp" - "gopkg.in/gographics/imagick.v3/imagick" + "gopkg.in/gographics/imagick.v2/imagick" "log" "net/http" "os" @@ -55,6 +55,7 @@ func main() { runner := shared.Runner{} runner.RunParallel(func() { + log.Printf("starting metrics server") if err := metrics.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Printf("error in metrics server: %s", err.Error()) } @@ -62,6 +63,7 @@ func main() { srv.Shutdown() }) runner.RunParallel(func() { + log.Printf("starting asynq server") if err := srv.Run(mux); err != nil { log.Printf("error in asynq server: %s", err.Error()) } diff --git a/backend/process.go b/backend/process.go index a934357c3df314a8dff7e2b6fe0cd81ab7ccc1bc..75bbf9880c50f753a947c4e3e0a356a4c601997b 100644 --- a/backend/process.go +++ b/backend/process.go @@ -7,6 +7,7 @@ import ( "git.kuschku.de/justjanne/imghost/shared" "github.com/hibiken/asynq" "github.com/prometheus/client_golang/prometheus" + "log" "os" "path/filepath" "strings" @@ -21,12 +22,15 @@ func trackTimeSince(counter prometheus.Counter, start time.Time) time.Time { func ProcessImageHandler(config *shared.Config) asynq.HandlerFunc { return func(ctx context.Context, t *asynq.Task) error { + log.Printf("received image resize task") task := shared.ImageTaskPayload{} if err := json.Unmarshal(t.Payload(), &task); err != nil { return err } + log.Printf("starting image resize task %s", task.ImageId) errors := ResizeImage(config, task.ImageId) + log.Printf("deleting cached image for image resize task %s", task.ImageId) _ = os.Remove(filepath.Join(config.SourceFolder, task.ImageId)) errorMessages := make([]string, len(errors)) @@ -35,6 +39,14 @@ func ProcessImageHandler(config *shared.Config) asynq.HandlerFunc { } if len(errors) != 0 { + log.Printf("errors occured while processing image resize task %s: %s", task.ImageId, strings.Join(errorMessages, "\n")) + if err := json.NewEncoder(t.ResultWriter()).Encode(shared.Result{ + Id: task.ImageId, + Success: true, + Errors: errorMessages, + }); err != nil { + return err + } return fmt.Errorf( "errors occured while processing task %s (%s): %s", t.Type(), @@ -43,6 +55,13 @@ func ProcessImageHandler(config *shared.Config) asynq.HandlerFunc { ) } + if err := json.NewEncoder(t.ResultWriter()).Encode(shared.Result{ + Id: task.ImageId, + Success: true, + Errors: []string{}, + }); err != nil { + return err + } return nil } } diff --git a/backend/resize.go b/backend/resize.go index 88fbd662badf722762d34e1247e358bad3418c14..b4b7ef53fa1d3d27e94e8cb8d6605fb63baa89b0 100644 --- a/backend/resize.go +++ b/backend/resize.go @@ -2,9 +2,11 @@ package main import ( "fmt" + "git.kuschku.de/justjanne/imghost/imgconv" "git.kuschku.de/justjanne/imghost/shared" - "github.com/justjanne/imgconv" - "gopkg.in/gographics/imagick.v3/imagick" + "gopkg.in/gographics/imagick.v2/imagick" + "log" + "os" "path/filepath" "time" ) @@ -12,37 +14,52 @@ import ( func ResizeImage(config *shared.Config, imageId string) []error { var err error + log.Printf("creating magick wand for %s", imageId) wand := imagick.NewMagickWand() defer wand.Destroy() startRead := time.Now().UTC() + log.Printf("reading image for %s", imageId) if err = wand.ReadImage(filepath.Join(config.SourceFolder, imageId)); err != nil { return []error{err} } + log.Printf("importing image for %s", imageId) var originalImage imgconv.ImageHandle if originalImage, err = imgconv.NewImage(wand); err != nil { return []error{err} } trackTimeSince(imageProcessDurationRead, startRead) + log.Printf("launching resize goroutines for %s", imageId) return runMany(len(config.Sizes), func(index int) error { definition := config.Sizes[index] path := filepath.Join(config.TargetFolder, fmt.Sprintf("%s%s", imageId, definition.Suffix)) startClone := time.Now().UTC() + log.Printf("cloning image for %s in %v", imageId, definition) image := originalImage.CloneImage() startCrop := trackTimeSince(imageProcessDurationClone, startClone) + log.Printf("cropping image for %s in %v", imageId, definition) if err := image.Crop(definition.Size); err != nil { return err } startResize := trackTimeSince(imageProcessDurationCrop, startCrop) + log.Printf("resizing image for %s in %v", imageId, definition) if err := image.Resize(definition.Size); err != nil { return err } startWrite := trackTimeSince(imageProcessDurationResize, startResize) - if err := image.Write(config.Quality, path); err != nil { + log.Printf("opening image for %s in %v", imageId, definition) + target, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { return err } + log.Printf("writing image for %s in %v", imageId, definition) + if err := image.WriteImageFile(config.Quality, target); err != nil { + return err + } + log.Printf("tracking time for %s in %v", imageId, definition) trackTimeSince(imageProcessDurationWrite, startWrite) + log.Printf("done with image for %s in %v", imageId, definition) return nil }) } diff --git a/cli/main.go b/cli/main.go new file mode 100644 index 0000000000000000000000000000000000000000..1d33374167c318470ec75adf5eb975101353697b --- /dev/null +++ b/cli/main.go @@ -0,0 +1,110 @@ +package main + +import ( + "encoding/json" + "flag" + "git.kuschku.de/justjanne/imghost/imgconv" + "gopkg.in/gographics/imagick.v2/imagick" + "io/fs" + "io/ioutil" + "os" +) + +type arguments struct { + Width *uint + Height *uint + Fit *string + Quality *uint + Source string + Target string + ExportMetadata *string +} + +var args = arguments{ + Width: flag.Uint( + "width", + 0, + "Desired width of the image", + ), + Height: flag.Uint( + "height", + 0, + "Desired height of the image", + ), + Fit: flag.String( + "fit", + "contain", + "Desired fit format for image. Allowed are cover and contain.", + ), + Quality: flag.Uint( + "quality", + 90, + "Desired quality of output image", + ), + ExportMetadata: flag.String( + "export-metadata", + "", + "Export metadata as json", + ), +} + +func main() { + flag.Parse() + if flag.NArg() < 2 { + flag.Usage() + os.Exit(1) + } + + imagick.Initialize() + defer imagick.Terminate() + + source := flag.Arg(0) + target := flag.Arg(1) + + data, err := convert(source, target, imgconv.Quality{ + CompressionQuality: *args.Quality, + SamplingFactors: []float64{1.0, 1.0, 1.0, 1.0}, + }, imgconv.Size{ + Width: *args.Width, + Height: *args.Height, + Format: *args.Fit, + }) + if err != nil { + panic(err) + } + + if *args.ExportMetadata != "" { + marshalled, err := json.MarshalIndent(data, "", " ") + if err != nil { + panic(err) + } + if err := ioutil.WriteFile(*args.ExportMetadata, marshalled, fs.FileMode(644)); err != nil { + panic(err) + } + } +} + +func convert(source string, target string, quality imgconv.Quality, size imgconv.Size) (*imgconv.Metadata, error) { + wand := imagick.NewMagickWand() + defer wand.Destroy() + + var err error + if err = wand.ReadImage(source); err != nil { + return nil, err + } + var image imgconv.ImageHandle + if image, err = imgconv.NewImage(wand); err != nil { + return nil, err + } + data := image.ParseMetadata() + if err := image.Crop(size); err != nil { + return nil, err + } + if err := image.Resize(size); err != nil { + return nil, err + } + if err := image.Write(quality, target); err != nil { + return nil, err + } + return &data, nil +} diff --git a/frontend/main.go b/frontend/main.go index 31ef61448eb87b1ebde1797ab82ef9ea89d1b688..7cf9e48a3ce4e0751033fef76f8bf4c5730aae47 100644 --- a/frontend/main.go +++ b/frontend/main.go @@ -29,6 +29,7 @@ func main() { context.Background(), &config, asynq.NewClient(config.AsynqOpts()), + asynq.NewInspector(config.AsynqOpts()), config.UploadTimeoutDuration(), db, http.FileServer(http.Dir(config.TargetFolder)), diff --git a/frontend/page_image_detail.go b/frontend/page_image_detail.go index 242de0189715c1e93a7f27a838746f8cb4ec0bd8..b1d8547eb70b61d4398ad9d8e26fdcd91d10d6be 100644 --- a/frontend/page_image_detail.go +++ b/frontend/page_image_detail.go @@ -10,9 +10,10 @@ import ( ) type ImageDetailData struct { - User UserInfo - Image shared.Image - IsMine bool + User UserInfo + Image shared.Image + IsMine bool + BaseUrl string } func pageImageDetail(ctx PageContext) http.Handler { @@ -86,6 +87,7 @@ func pageImageDetail(ctx PageContext) http.Handler { user, info, owner == user.Id, + ctx.Config.BaseUrl, }); err != nil { formatError(w, ErrorData{http.StatusInternalServerError, user, r.URL, err}, "html") return diff --git a/frontend/page_upload.go b/frontend/page_upload.go index 0f94f5d7a414187011f4d37087ec661e736f2fef..24f37e14be27a2075f91f490aa3bd4ff4446eeae 100644 --- a/frontend/page_upload.go +++ b/frontend/page_upload.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "git.kuschku.de/justjanne/imghost/shared" + "github.com/hibiken/asynq" "io" "mime/multipart" "net/http" @@ -14,6 +15,11 @@ import ( "time" ) +type UploadData struct { + BaseUrl string + User UserInfo +} + func detectMimeType(path string) (string, error) { file, err := os.Open(path) if err != nil { @@ -100,19 +106,21 @@ func pageUpload(ctx PageContext) http.Handler { return } - fmt.Printf("Created task %s at %d\n", image.Id, time.Now().Unix()) + fmt.Printf("created task %s at %d\n", image.Id, time.Now().Unix()) t, err := shared.NewImageResizeTask(image.Id) - fmt.Printf("Submitted task %s at %d\n", image.Id, time.Now().Unix()) if err != nil { formatError(w, ErrorData{http.StatusInternalServerError, user, r.URL, err}, "json") return } - info, err := ctx.Async.Enqueue(t) + info, err := ctx.AsynqClient.Enqueue(t, asynq.Retention(ctx.UploadTimeout)) + fmt.Printf("submitted task %s at %d\n", image.Id, time.Now().Unix()) if err != nil { formatError(w, ErrorData{http.StatusInternalServerError, user, r.URL, err}, "json") return } - if err := waitOnTask(info, ctx.UploadTimeout); err != nil { + info, err = waitOnTask(ctx, info, ctx.UploadTimeout) + fmt.Printf("got result for task %s at %d\n", image.Id, time.Now().Unix()) + if err != nil { formatError(w, ErrorData{http.StatusInternalServerError, user, r.URL, err}, "json") return } @@ -128,7 +136,8 @@ func pageUpload(ctx PageContext) http.Handler { return } else { user := parseUser(r) - if err := formatTemplate(w, "upload.html", IndexData{ + if err := formatTemplate(w, "upload.html", UploadData{ + ctx.Config.BaseUrl, user, }); err != nil { formatError(w, ErrorData{http.StatusInternalServerError, user, r.URL, err}, "html") diff --git a/frontend/templates/image_detail.html b/frontend/templates/image_detail.html index 7bca8fb5236fa1a9d7ed2d0f318fbceef39ddc56..6c4519585bb99569c75c64bf3e1be5c445b4bc8b 100644 --- a/frontend/templates/image_detail.html +++ b/frontend/templates/image_detail.html @@ -38,20 +38,20 @@ <div class="url"> <p>Detail Page</p> <div> - <input id="url_full" type="text" value="https://i.k8r.eu/i/{{.Image.Id}}"> + <input id="url_full" type="text" value="{{ .BaseUrl }}/i/{{.Image.Id}}"> <button class="copy" data-target="#url_full">Copy</button> </div> </div> <div class="url"> <p>Direct Link</p> <div> - <input id="url_direct" type="text" value="https://i.k8r.eu/{{.Image.Id}}.png"> + <input id="url_direct" type="text" value="{{ .BaseUrl }}/{{.Image.Id}}.png"> <button class="copy" data-target="#url_direct">Copy</button> </div> </div> </div> </div> {{if .IsMine}} -<script src="/assets/js/page_image_detail.js"></script> + <script src="/assets/js/page_image_detail.js"></script> {{end}} {{end}} diff --git a/frontend/templates/image_list.html b/frontend/templates/image_list.html index ce33d65d294af6ad3a8de052eb6e8268b5aa0fe2..ef3784a0669fc5a4fad463fff3c1d7cc725800a5 100644 --- a/frontend/templates/image_list.html +++ b/frontend/templates/image_list.html @@ -3,19 +3,19 @@ {{define "content"}} <div class="page image list"> {{range .Images}} - <a class="image" href="/i/{{.Id}}"> - <div class="image-container"> - <img src="https://i.k8r.eu/{{.Id}}t.png"> - </div> - <div class="info"> - <p class="title"> - {{- if eq .Title "" -}} - <span class="placeholder">Unnamed</span> - {{- else -}} - {{.Title}} - {{- end -}} - </p> - <p>{{.OriginalName}}</p> + <a class="image" href="/i/{{.Id}}"> + <div class="image-container"> + <img src="/{{.Id}}t.png"> + </div> + <div class="info"> + <p class="title"> + {{- if eq .Title "" -}} + <span class="placeholder">Unnamed</span> + {{- else -}} + {{.Title}} + {{- end -}} + </p> + <p>{{.OriginalName}}</p> <p> <time>{{.CreatedAt.Format "2006-01-02 15:04"}}</time> </p> diff --git a/frontend/templates/upload.html b/frontend/templates/upload.html index 6508d46af09afb2a90a8fe3d9c87974901a968c9..6f20fb17e751b1c0181947c7428b15ef54af014b 100644 --- a/frontend/templates/upload.html +++ b/frontend/templates/upload.html @@ -1,26 +1,27 @@ +{{- /*gotype: git.kuschku.de/justjanne/imghost.UploadData*/ -}} {{define "title"}}Upload | ik8r{{end}} {{define "content"}} <div class="page upload"> - <div class="container centered"> - <form class="upload" action="/upload/" method="POST" enctype="multipart/form-data"> - <label> - <span class="text">Select Files</span> - <input type="file" name="file" accept=".jpg,.jpeg,.png,.gif,.apng,.tiff,.tif,.bmp,.webp,.mp4,.mov" multiple/> - </label> - </form> - </div> - <div class="uploading-images"> - <div class="images"></div> - <div class="sidebar"> - <div class="url"> - <p>Album</p> - <div> - <input id="url_full" type="text" value="https://i.k8r.eu/a/ERROR"> - <button class="copy" data-target="#url_full">Copy</button> - </div> - </div> + <div class="container centered"> + <form class="upload" action="/upload/" method="POST" enctype="multipart/form-data"> + <label> + <span class="text">Select Files</span> + <input type="file" name="file" accept=".jpg,.jpeg,.png,.gif,.apng,.tiff,.tif,.bmp,.webp,.mp4,.mov" multiple/> + </label> + </form> + </div> + <div class="uploading-images"> + <div class="images"></div> + <div class="sidebar"> + <div class="url"> + <p>Album</p> + <div> + <input id="url_full" type="text" value="{{ .BaseUrl }}/a/ERROR"> + <button class="copy" data-target="#url_full">Copy</button> </div> + </div> </div> + </div> </div> <script src="/assets/js/page_upload.js"></script> -{{end}} \ No newline at end of file +{{end}} diff --git a/frontend/util.go b/frontend/util.go index 1aac113a0ceccafad60b11cef14b056daa7b5708..af6fe5cbbde265a243731c39fb9b7b6be20697a1 100644 --- a/frontend/util.go +++ b/frontend/util.go @@ -9,7 +9,6 @@ import ( "github.com/hibiken/asynq" "html/template" "net/http" - "strings" "time" ) @@ -30,13 +29,14 @@ func (info UserInfo) HasRole(role string) bool { } type PageContext struct { - Context context.Context - Config *shared.Config - Async *asynq.Client - UploadTimeout time.Duration - Database *sql.DB - Images http.Handler - AssetServer http.Handler + Context context.Context + Config *shared.Config + AsynqClient *asynq.Client + AsynqInspector *asynq.Inspector + UploadTimeout time.Duration + Database *sql.DB + Images http.Handler + AssetServer http.Handler } type AlbumImage struct { @@ -56,10 +56,10 @@ type Album struct { func parseUser(r *http.Request) UserInfo { return UserInfo{ - r.Header.Get("X-Auth-Subject"), - r.Header.Get("X-Auth-Username"), - r.Header.Get("X-Auth-Email"), - strings.Split(r.Header.Get("X-Auth-Roles"), ","), + "d41d9763-04a2-435a-b1eb-f636a81ef728", + "testuser", + "test@example.com", + []string{"imghost:user"}, } } @@ -106,27 +106,41 @@ func formatTemplate(w http.ResponseWriter, templateName string, data interface{} return nil } -func waitOnTask(info *asynq.TaskInfo, timeout time.Duration) error { +func waitOnTask(ctx PageContext, info *asynq.TaskInfo, timeout time.Duration) (*asynq.TaskInfo, error) { total := time.Duration(0) - for total < timeout && info.State != asynq.TaskStateCompleted { - for info.State == asynq.TaskStateScheduled { - duration := info.NextProcessAt.Sub(time.Now()) - total += duration - if total < timeout { - time.Sleep(duration) - } + info, err := ctx.AsynqInspector.GetTaskInfo(info.Queue, info.ID) + if err != nil { + return nil, err + } + // Wait for it being scheduled + for total < timeout && info.State == asynq.TaskStateScheduled { + duration := info.NextProcessAt.Sub(time.Now()) + total += duration + if total < timeout { + time.Sleep(duration) + } + info, err = ctx.AsynqInspector.GetTaskInfo(info.Queue, info.ID) + if err != nil { + return nil, err + } + } + // Wait for it being completed + for total < timeout && info.State != asynq.TaskStateArchived && info.State != asynq.TaskStateCompleted { + duration := 1 * time.Second + total += duration + if total < timeout { + time.Sleep(duration) } - if info.State != asynq.TaskStateCompleted { - duration := time.Duration(1 * time.Second) - total += duration - if total < timeout { - time.Sleep(duration) - } + info, err = ctx.AsynqInspector.GetTaskInfo(info.Queue, info.ID) + if err != nil { + return nil, err } } if info.State == asynq.TaskStateCompleted { - return nil + return info, nil + } else if info.State == asynq.TaskStateArchived { + return info, fmt.Errorf("error executing task: %s (%s), has status %s", info.Type, info.ID, info.State) } else { - return fmt.Errorf("timed out waiting on task: %s (%s)", info.Type, info.ID) + return info, fmt.Errorf("task timed out: %s (%s), has status %s", info.Type, info.ID, info.State) } } diff --git a/go.mod b/go.mod index a47887b505b089205226453424fc28298f538745..c524d28cad7ea76991b56d7e5dd0026788317ed0 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,9 @@ module git.kuschku.de/justjanne/imghost go 1.15 require ( - github.com/go-redis/redis/v8 v8.11.5 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/hibiken/asynq v0.23.0 - github.com/justjanne/imgconv v1.4.1 github.com/lib/pq v1.10.4 github.com/prometheus/client_golang v1.11.1 - github.com/spf13/cast v1.5.0 // indirect - golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 - golang.org/x/time v0.0.0-20220411224347-583f2d630306 // indirect - google.golang.org/protobuf v1.28.0 // indirect - gopkg.in/gographics/imagick.v3 v3.4.0 - gopkg.in/yaml.v2 v2.4.0 + gopkg.in/gographics/imagick.v2 v2.4.0 + gopkg.in/yaml.v2 v2.3.0 ) diff --git a/go.sum b/go.sum index 1ac29945c336089da3d197e64cb89caeceec6f2f..70ec140d5fab730fdf3518b659d153947367e147 100644 --- a/go.sum +++ b/go.sum @@ -11,14 +11,9 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.1.2 h1:YRXhKfTDauu4ajMg1TPgFO5jnlC2HCbmLXMcTG5cbYE= -github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -26,8 +21,6 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= @@ -37,11 +30,9 @@ github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vb github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-redis/redis/v8 v8.11.2 h1:WqlSpAwz8mxDSMCvbyz1Mkiqe0LE5OY4j3lgkvu1Ts0= github.com/go-redis/redis/v8 v8.11.2/go.mod h1:DLomh7y2e3ggQXQLd1YgmvIfecPJoFl7WU5SOQ/r06M= -github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI= -github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= @@ -55,10 +46,8 @@ github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:W github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -66,36 +55,28 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs= github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hibiken/asynq v0.23.0 h1:kmKkNFgqiXBatC8oz94Mer6uvKoGn4STlIVDV5wnKyE= github.com/hibiken/asynq v0.23.0/go.mod h1:K70jPVx+CAmmQrXot7Dru0D52EO7ob4BIun3ri5z1Qw= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/justjanne/imgconv v1.4.1 h1:byVibYVhfJZeQAaWwsU3EenvPOIf5OD2pnimc56IbS0= -github.com/justjanne/imgconv v1.4.1/go.mod h1:6uRn+br2dIv8K5G9iEVjAmBan0/GAW2++OEAVDuk0GE= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.10.4 h1:SO9z7FRPzA03QhHKJrH5BXA6HU1rS4V2nIVrrNC1iYk= github.com/lib/pq v1.10.4/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -106,23 +87,16 @@ github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lN github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nxadm/tail v1.4.4 h1:DQuhQpB1tVlglWS2hLQ5OV6B5r8aGxSrPc5Qo6uTN78= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= +github.com/onsi/ginkgo v1.15.0 h1:1V1NfVQR87RtWAgp1lv9JZJ5Jap+XFGKPi00andXGi4= github.com/onsi/ginkgo v1.15.0/go.mod h1:hF8qUzuuC8DJGygJH3726JnCZX4MYbRB8yFfISqnKUg= -github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/ginkgo/v2 v2.0.0 h1:CcuG/HvWNkkaqCUpJifQY8z7qEMBJya6aLPx6ftGyjQ= -github.com/onsi/ginkgo/v2 v2.0.0/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= +github.com/onsi/gomega v1.10.5 h1:7n6FEkpFmfCoo2t+YYqXH0evK+a9ICQz0xcAy9dYcaQ= github.com/onsi/gomega v1.10.5/go.mod h1:gza4q3jKQJijlu05nKWRCW/GavJumGt8aNRxWg7mt48= -github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.18.1 h1:M1GfJqGRrBrrGGsbxzV5dqM2U2ApXefZCQpkukxYRLE= -github.com/onsi/gomega v1.18.1/go.mod h1:0q+aL8jAiMXy9hbwj2mr5GziHiwhAIQpFmmtT5hitRs= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -149,20 +123,16 @@ github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3x github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= -github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -190,9 +160,8 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb h1:eBmm0M9fYhWpKZLjQUUKka/LtIxf46G4fxeEz5KJr9U= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -212,29 +181,21 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220411224347-583f2d630306 h1:+gHMid33q6pen7kv9xvT+JRinntgeXO2AeZVd0AWD3w= -golang.org/x/time v0.0.0-20220411224347-583f2d630306/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -264,28 +225,23 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0 h1:w43yiav+6bVFTBQFZX0r7ipe9JQ1QsbMgHwbBziscLw= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/gographics/imagick.v3 v3.4.0 h1:kSnbsXOWofo81VJEn/Hw8w3qqoOrfTyWwjAQzSdtPlg= -gopkg.in/gographics/imagick.v3 v3.4.0/go.mod h1:+Q9nyA2xRZXrDyTtJ/eko+8V/5E7bWYs08ndkZp8UmA= +gopkg.in/gographics/imagick.v2 v2.4.0 h1:E1KeUJAk9TUdhClfkvWPjJbDfXe1R946J6vgBtPVP4Q= +gopkg.in/gographics/imagick.v2 v2.4.0/go.mod h1:of4TbGX8yMcpgWkWFjha7FsOFr+NjOJ5O1qtKU27Yj0= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/imgconv/imagehandle.go b/imgconv/imagehandle.go new file mode 100644 index 0000000000000000000000000000000000000000..427b77b6fccfcea5aa8fa16733fb71f995610431 --- /dev/null +++ b/imgconv/imagehandle.go @@ -0,0 +1,225 @@ +package imgconv + +import ( + "gopkg.in/gographics/imagick.v2/imagick" + "log" + "math" + "os" + "strings" +) + +func NewImage(wand *imagick.MagickWand) (ImageHandle, error) { + meta := ImageHandle{ + wand: wand, + depth: wand.GetImageDepth(), + } + + if err := wand.AutoOrientImage(); err != nil { + return meta, err + } + + if len(wand.GetImageProfiles("i*")) == 0 { + if err := wand.ProfileImage("icc", ProfileSRGB); err != nil { + return meta, err + } + } + + for _, name := range wand.GetImageProfiles("*") { + meta.profiles = append(meta.profiles, ColorProfile{ + data: []byte(wand.GetImageProfile(name)), + format: name, + }) + } + if meta.depth < 16 { + if err := wand.SetImageDepth(16); err != nil { + return meta, err + } + } + if err := wand.ProfileImage("icc", ProfileACESLinear); err != nil { + return meta, err + } + return meta, nil +} + +func (image *ImageHandle) CloneImage() ImageHandle { + return ImageHandle{ + image.wand.Clone(), + image.depth, + image.profiles, + } +} + +func (image *ImageHandle) ParseMetadata() Metadata { + return parseMetadata(image.wand) +} + +func (image *ImageHandle) SanitizeMetadata() error { + var profiles []ColorProfile + for _, profile := range image.profiles { + if !strings.EqualFold("exif", profile.format) { + profiles = append(profiles, profile) + } + } + image.profiles = profiles + image.wand.RemoveImageProfile("exif") + + if err := image.wand.SetOption("png:include-chunk", "bKGD,cHRM,iCCP"); err != nil { + return err + } + if err := image.wand.SetOption("png:exclude-chunk", "EXIF,iTXt,tEXt,zTXt,date"); err != nil { + return err + } + for _, key := range image.wand.GetImageProperties("png:*") { + if err := image.wand.DeleteImageProperty(key); err != nil { + return err + } + } + + return nil +} + +func (image *ImageHandle) Crop(size Size) error { + if size.Width == 0 || size.Height == 0 || size.Format != ImageFitCover { + return nil + } + + currentWidth := image.wand.GetImageWidth() + currentHeight := image.wand.GetImageHeight() + + currentAspectRatio := float64(currentWidth) / float64(currentHeight) + desiredAspectRatio := float64(size.Width) / float64(size.Height) + + if currentAspectRatio == desiredAspectRatio { + return nil + } + + var desiredWidth, desiredHeight uint + if desiredAspectRatio > currentAspectRatio { + desiredWidth = currentWidth + desiredHeight = uint(math.Round(float64(currentWidth) / desiredAspectRatio)) + } else { + desiredHeight = currentHeight + desiredWidth = uint(math.Round(desiredAspectRatio * float64(currentHeight))) + } + + offsetLeft := int((currentWidth - desiredWidth) / 2.0) + offsetTop := int((currentHeight - desiredHeight) / 2.0) + + if err := image.wand.CropImage(desiredWidth, desiredHeight, offsetLeft, offsetTop); err != nil { + return err + } + + return nil +} + +func determineDesiredSize(width uint, height uint, size Size) (uint, uint) { + currentAspectRatio := float64(width) / float64(height) + + var desiredWidth, desiredHeight uint + if size.Height != 0 && size.Width != 0 { + if size.Format == ImageFitCover { + var desiredAspectRatio = float64(size.Width) / float64(size.Height) + var croppedWidth, croppedHeight uint + if desiredAspectRatio > currentAspectRatio { + croppedWidth = width + croppedHeight = uint(math.Round(float64(width) / desiredAspectRatio)) + } else { + croppedHeight = height + croppedWidth = uint(math.Round(desiredAspectRatio * float64(height))) + } + + desiredHeight = uint(math.Min(float64(size.Height), float64(croppedHeight))) + desiredWidth = uint(math.Min(float64(size.Width), float64(croppedWidth))) + } else if currentAspectRatio > 1 { + desiredWidth = uint(math.Min(float64(size.Width), float64(width))) + desiredHeight = uint(math.Round(float64(desiredWidth) / currentAspectRatio)) + } else { + desiredHeight = uint(math.Min(float64(size.Height), float64(height))) + desiredWidth = uint(math.Round(currentAspectRatio * float64(desiredHeight))) + } + } else if size.Height != 0 { + desiredHeight = uint(math.Min(float64(size.Height), float64(height))) + desiredWidth = uint(math.Round(currentAspectRatio * float64(desiredHeight))) + } else if size.Width != 0 { + desiredWidth = uint(math.Min(float64(size.Width), float64(width))) + desiredHeight = uint(math.Round(float64(desiredWidth) / currentAspectRatio)) + } else { + desiredWidth = width + desiredHeight = height + } + + return desiredWidth, desiredHeight +} + +func (image *ImageHandle) Resize(size Size) error { + if size.Width == 0 && size.Height == 0 { + return nil + } + + currentWidth := image.wand.GetImageWidth() + currentHeight := image.wand.GetImageHeight() + + desiredWidth, desiredHeight := determineDesiredSize(currentWidth, currentHeight, size) + + if desiredWidth != currentWidth || desiredHeight != currentHeight { + if err := image.wand.ResizeImage(desiredWidth, desiredHeight, imagick.FILTER_LANCZOS, 1); err != nil { + return err + } + } + + return nil +} + +func (image *ImageHandle) prepareWrite(quality Quality) error { + log.Printf("preparing image for writing at quality %v", quality) + for _, profile := range image.profiles { + log.Printf("setting color profile on image '%s' %d", profile.format, len(profile.data)) + if err := image.wand.ProfileImage(profile.format, profile.data); err != nil { + return err + } + } + log.Printf("setting image depth on image %d", image.depth) + if err := image.wand.SetImageDepth(image.depth); err != nil { + return err + } + + if quality.CompressionQuality != 0 { + log.Printf("setting compression quality on image %d", quality.CompressionQuality) + if err := image.wand.SetImageCompressionQuality(quality.CompressionQuality); err != nil { + return err + } + } + + if len(quality.SamplingFactors) != 0 { + log.Printf("setting sampling factors on image %v", quality.SamplingFactors) + if err := image.wand.SetSamplingFactors(quality.SamplingFactors); err != nil { + return err + } + } + + log.Printf("done preparing image for writing") + + return nil +} + +func (image *ImageHandle) Write(quality Quality, target string) error { + if err := image.prepareWrite(quality); err != nil { + return err + } + log.Printf("writing image %s", target) + if err := image.wand.WriteImage(target); err != nil { + return err + } + return nil +} + +func (image *ImageHandle) WriteImageFile(quality Quality, target *os.File) error { + if err := image.prepareWrite(quality); err != nil { + return err + } + log.Printf("writing image %s", target.Name()) + if err := image.wand.WriteImageFile(target); err != nil { + return err + } + return nil +} diff --git a/imgconv/metadata.go b/imgconv/metadata.go new file mode 100644 index 0000000000000000000000000000000000000000..84509adf39651f4a0793ac79b59777522ee152c9 --- /dev/null +++ b/imgconv/metadata.go @@ -0,0 +1,884 @@ +package imgconv + +import ( + "encoding/json" + "fmt" + "gopkg.in/gographics/imagick.v2/imagick" + "strconv" + "strings" + "time" +) + +type Metadata struct { + AspectRatio *Ratio `json:"aspectRatio,omitempty"` + Make string `json:"make,omitempty"` + Model string `json:"model,omitempty"` + LensMake string `json:"lensMake,omitempty"` + LensModel string `json:"lensModel,omitempty"` + Software string `json:"software,omitempty"` + Copyright string `json:"copyright,omitempty"` + Description string `json:"description,omitempty"` + CreatedAt *time.Time `json:"createdAt,omitempty"` + DigitizedAt *time.Time `json:"digitizedAt,omitempty"` + OriginalAt *time.Time `json:"originalAt,omitempty"` + DigitalZoom *Ratio `json:"digitalZoom,omitempty"` + Exposure *Ratio `json:"exposure,omitempty"` + ExposureMode ExposureMode `json:"exposureMode,omitempty"` + ExposureProgram ExposureProgram `json:"exposureProgram,omitempty"` + ShutterSpeed *Ratio `json:"shutterSpeed,omitempty"` + Aperture *Ratio `json:"aperture,omitempty"` + Brightness *Ratio `json:"brightness,omitempty"` + MaxAperture *Ratio `json:"maxAperture,omitempty"` + Flash *Flash `json:"flash,omitempty"` + FocalLength *Ratio `json:"focalLength,omitempty"` + FocalLengthFF *Ratio `json:"focalLengthFF,omitempty"` + IsoSpeedRating *int64 `json:"isoSpeedRating,omitempty"` + LightSource LightSource `json:"lightSource,omitempty"` + MeteringMode MeteringMode `json:"meteringMode,omitempty"` + WhiteBalance WhiteBalance `json:"whiteBalance,omitempty"` + SceneMode SceneMode `json:"scene,omitempty"` + ISO *int64 `json:"iso,omitempty"` + Orientation Orientation `json:"orientation,omitempty"` + Contrast ContrastMode `json:"contrast,omitempty"` + Sharpness SharpnessMode `json:"sharpness,omitempty"` + SubjectDistance *Ratio `json:"subjectDistance,omitempty"` + SubjectDistanceRange DistanceRange `json:"subjectDistanceRange,omitempty"` + FileSource FileSource `json:"source,omitempty"` + Saturation *int64 `json:"saturation,omitempty"` + SensorType SensorType `json:"sensor,omitempty"` + LensSpecification *LensSpecification `json:"lensSpecification,omitempty"` + Location *Location `json:"location,omitempty"` + Resolution *Resolution `json:"resolution,omitempty"` +} + +func parseMetadata(wand *imagick.MagickWand) Metadata { + keys := exifKeyMap(wand) + + get := func(key string) string { + originalKey, ok := keys[key] + if ok { + return wand.GetImageProperty(originalKey) + } else { + return "" + } + } + + return Metadata{ + AspectRatio: &Ratio{ + int64(wand.GetImageWidth()), + int64(wand.GetImageHeight()), + }, + Make: strings.TrimSpace(get("Make")), + Model: strings.TrimSpace(get("Model")), + LensMake: strings.TrimSpace(get("LensMake")), + LensModel: strings.TrimSpace(get("LensModel")), + Software: strings.TrimSpace(get("Software")), + Copyright: strings.TrimSpace(get("Copyright")), + Description: strings.TrimSpace(get("ImageDescription")), + CreatedAt: parseTime(get("DateTime"), get("SubSecTime")), + DigitizedAt: parseTime(get("DateTimeDigitized"), get("SubSecTimeDigitized")), + OriginalAt: parseTime(get("DateTimeOriginal"), get("SubSecTimeOriginal")), + DigitalZoom: parseRatio(get("DigitalZoomRatio")), + Exposure: parseRatio(get("ExposureBiasValue")), + ExposureMode: parseExposureMode(parseNumber(get("ExposureMode"))), + ExposureProgram: parseExposureProgram(parseNumber(get("ExposureProgram"))), + ShutterSpeed: parseShutterSpeed(parseRatio(get("ExposureTime")), parseRatio(get("ShutterSpeedValue"))), + Aperture: parseRatio(get("FNumber")), + Brightness: parseRatio(get("BrightnessValue")), + MaxAperture: parseRatio(get("MaxApertureValue")), + Flash: parseFlash(parseNumber(get("Flash")), get("FlashEnergy")), + FocalLength: parseRatio(get("FocalLength")), + FocalLengthFF: parseRatio(get("FocalLengthIn35mmFilm")), + ISO: parseNumber(get("PhotographicSensitivity")), + LightSource: parseLightSource(parseNumber(get("LightSource"))), + MeteringMode: parseMeteringMode(parseNumber(get("MeteringMode"))), + Orientation: parseOrientation(parseNumber(get("Orientation"))), + WhiteBalance: parseWhiteBalance(parseNumber(get("WhiteBalance"))), + SceneMode: parseSceneMode(parseNumber(get("SceneMode"))), + Contrast: parseContrastMode(parseNumber(get("Contrast"))), + Sharpness: parseSharpnessMode(parseNumber(get("Sharpness"))), + SubjectDistance: parseRatio(get("SubjectDistance")), + SubjectDistanceRange: parseDistanceRange(parseNumber(get("SubjectDistanceRange"))), + FileSource: parseFileSource(parseNumber(get("FileSource"))), + Saturation: parseNumber(get("Saturation")), + SensorType: parseSensorType(parseNumber(get("SensingMethod"))), + LensSpecification: parseLensSpecification(get("LensSpecification")), + Location: parseLocation( + parseCoordinate(get("GPSLatitude"), get("GPSLatitudeRef")), + parseCoordinate(get("GPSLongitude"), get("GPSLongitudeRef")), + ), + Resolution: parseResolution( + parseNumber(get("ResolutionUnit")), + parseRatio(get("XResolution")), + parseRatio(get("YResolution")), + ), + } +} + +func exifKeyMap(wand *imagick.MagickWand) map[string]string { + metadata := make(map[string]string) + for _, key := range wand.GetImageProperties("exif:*") { + if strings.HasPrefix(key, "exif:thumbnail:") { + continue + } + trimmedKey := strings.TrimPrefix(key, "exif:") + metadata[trimmedKey] = key + } + return metadata +} + +const ExifTime = "2006:01:02 15:04:05" + +func parseTime(value string, subSec string) *time.Time { + result, err := time.Parse(ExifTime, value) + if err != nil { + return nil + } else { + microseconds := parseNumber(subSec) + if microseconds != nil { + result = result.Add(time.Duration(*microseconds) * time.Microsecond) + } + return &result + } +} + +func parseNumber(value string) *int64 { + result, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return nil + } else { + return &result + } +} + +func parseShutterSpeed(exposure *Ratio, shutterSpeed *Ratio) *Ratio { + if shutterSpeed != nil { + return &Ratio{ + shutterSpeed.Denominator, + shutterSpeed.Numerator, + } + } + return exposure +} + +type Flash struct { + Available bool `json:"available"` + Fired bool `json:"fired"` + StrobeDetection StrobeDetection `json:"strobeDetection"` + Mode FlashMode `json:"mode,omitempty"` + RedEyeReduction bool `json:"redEyeReduction"` + Strength *Ratio `json:"strength,omitempty"` +} + +type StrobeDetection struct { + Available bool `json:"available"` + Detected bool `json:"detected"` +} + +const ( + maskFired = 0x0001 + maskStrobeDetected = 0x0002 + maskStrobeDetectionAvailable = 0x0004 + maskMode = 0x0003 + maskUnavailable = 0x0020 + maskRedEye = 0x0040 +) + +func parseFlash(flash *int64, strength string) *Flash { + if flash == nil { + return nil + } + + return &Flash{ + Available: *flash&maskUnavailable == 0, + Fired: *flash&maskFired != 0, + StrobeDetection: StrobeDetection{ + Available: *flash&maskStrobeDetectionAvailable != 0, + Detected: *flash&maskStrobeDetected != 0, + }, + Mode: parseFlashMode((*flash >> 3) & maskMode), + RedEyeReduction: *flash&maskRedEye != 0, + Strength: parseRatio(strength), + } +} + +type Resolution struct { + X Ratio `json:"x"` + Y Ratio `json:"y"` +} + +func parseResolution(unit *int64, x *Ratio, y *Ratio) *Resolution { + if unit == nil { + defaultUnit := int64(2) + unit = &defaultUnit + } + if x == nil || y == nil { + return nil + } + return &Resolution{ + X: Ratio{ + x.Numerator, + x.Denominator * *unit, + }.reduce(), + Y: Ratio{ + y.Numerator, + y.Denominator * *unit, + }.reduce(), + } +} + +type LensSpecification struct { + WideFocalLength *Ratio `json:"wideFocalLength,omitempty"` + WideAperture *Ratio `json:"wideAperture,omitempty"` + TeleFocalLength *Ratio `json:"teleFocalLength,omitempty"` + TeleAperture *Ratio `json:"teleAperture,omitempty"` +} + +func parseLensSpecification(value string) *LensSpecification { + split := strings.Split(value, ", ") + if len(split) != 4 { + return nil + } + return &LensSpecification{ + WideFocalLength: parseRatio(split[0]), + TeleFocalLength: parseRatio(split[1]), + WideAperture: parseRatio(split[2]), + TeleAperture: parseRatio(split[3]), + } +} + +type Location struct { + Longitude Coordinate `json:"longitude"` + Latitude Coordinate `json:"latitude"` +} + +func parseLocation(longitude *Coordinate, latitude *Coordinate) *Location { + if longitude == nil || latitude == nil { + return nil + } + return &Location{*longitude, *latitude} +} + +type Coordinate struct { + Degree Ratio `json:"degree"` + Minute Ratio `json:"minute"` + Second Ratio `json:"second"` + CardinalDirection CardinalDirection `json:"reference"` +} + +func parseCoordinate(value string, reference string) *Coordinate { + parts := strings.Split(value, ", ") + if len(parts) != 3 { + return nil + } + degree := parseRatio(parts[0]) + minute := parseRatio(parts[1]) + second := parseRatio(parts[2]) + direction := parseCardinalDirection(reference) + if degree == nil || minute == nil || second == nil || direction == "" { + return nil + } + return &Coordinate{ + *degree, + *minute, + *second, + direction, + } +} + +type CardinalDirection string + +const ( + CardinalDirectionNorth CardinalDirection = "N" + CardinalDirectionWest CardinalDirection = "W" + CardinalDirectionSouth CardinalDirection = "S" + CardinalDirectionEast CardinalDirection = "E" +) + +func parseCardinalDirection(value string) CardinalDirection { + switch value { + case "N": + return CardinalDirectionNorth + case "W": + return CardinalDirectionWest + case "S": + return CardinalDirectionSouth + case "E": + return CardinalDirectionEast + default: + return "" + } +} + +type Ratio struct { + Numerator int64 `json:"num"` + Denominator int64 `json:"den"` +} + +func (ratio *Ratio) MarshalJSON() ([]byte, error) { + return json.Marshal(fmt.Sprintf("%d/%d", ratio.Numerator, ratio.Denominator)) +} + +func (ratio *Ratio) UnmarshalJSON(data []byte) error { + var raw string + if err := json.Unmarshal(data, &raw); err != nil { + return err + } + split := strings.Split(raw, "/") + if len(split) != 2 { + fallback := parseNumber(raw) + if fallback == nil { + return fmt.Errorf("could not deserialize ratio: ratio is neither a ratio nor a plain number") + } else { + ratio.Numerator = *fallback + ratio.Denominator = 1 + return nil + } + } + numerator := parseNumber(split[0]) + if numerator == nil { + return fmt.Errorf("could not deserialize ratio: numerator is not a valid number") + } + denominator := parseNumber(split[1]) + if denominator == nil { + return fmt.Errorf("could not deserialize ratio: denominator is not a valid number") + } + + ratio.Numerator = *numerator + ratio.Denominator = *denominator + return nil +} + +func (ratio Ratio) reduce() Ratio { + if ratio.Numerator > 0 && ratio.Denominator > 0 && ratio.Numerator%ratio.Denominator == 0 { + ratio.Numerator = ratio.Numerator / ratio.Denominator + ratio.Denominator = 1 + } else if ratio.Numerator == 0 { + ratio.Numerator = 0 + ratio.Denominator = 1 + } + return ratio +} + +func parseRatio(value string) *Ratio { + split := strings.Split(value, "/") + if len(split) != 2 { + return nil + } + numerator := parseNumber(split[0]) + denominator := parseNumber(split[1]) + if numerator == nil || denominator == nil { + return nil + } + result := Ratio{*numerator, *denominator}.reduce() + return &result +} + +type SensorType string + +const ( + SensorTypeOther SensorType = "other" + SensorTypeSingleChipColorArea SensorType = "single_chip_color_area" + SensorTypeDualChipColorArea SensorType = "dual_chip_color_area" + SensorTypeTripleChipColorArea SensorType = "triple_chip_color_area" + SensorTypeColorSequentialArea SensorType = "color_sequential_area" + SensorTypeTrilinear SensorType = "trilinear" + SensorTypeColorSequentialLinear SensorType = "color_sequential_linear" + + valueSensorTypeOther = 1 + valueSensorTypeSingleChipColorArea = 2 + valueSensorTypeDualChipColorArea = 3 + valueSensorTypeTripleChipColorArea = 4 + valueSensorTypeColorSequentialArea = 5 + valueSensorTypeTrilinear = 7 + valueSensorTypeColorSequentialLinear = 8 +) + +func parseSensorType(value *int64) SensorType { + if value == nil { + return "" + } + + switch *value { + case valueSensorTypeOther: + return SensorTypeOther + case valueSensorTypeSingleChipColorArea: + return SensorTypeSingleChipColorArea + case valueSensorTypeDualChipColorArea: + return SensorTypeDualChipColorArea + case valueSensorTypeTripleChipColorArea: + return SensorTypeTripleChipColorArea + case valueSensorTypeColorSequentialArea: + return SensorTypeColorSequentialArea + case valueSensorTypeTrilinear: + return SensorTypeTrilinear + case valueSensorTypeColorSequentialLinear: + return SensorTypeColorSequentialLinear + default: + return "" + } +} + +type FileSource string + +const ( + FileSourceOther FileSource = "other" + FileSourceTransmissiveScanner FileSource = "scanner_transmissive" + FileSourceReflectiveScanner FileSource = "scanner_reflective" + FileSourceDigitalCamera FileSource = "digital_camera" + + valueFileSourceOther = 0 + valueFileSourceTransmissiveScanner = 1 + valueFileSourceReflectiveScanner = 2 + valueFileSourceDigitalCamera = 3 +) + +func parseFileSource(value *int64) FileSource { + if value == nil { + return "" + } + + switch *value { + case valueFileSourceOther: + return FileSourceOther + case valueFileSourceTransmissiveScanner: + return FileSourceTransmissiveScanner + case valueFileSourceReflectiveScanner: + return FileSourceReflectiveScanner + case valueFileSourceDigitalCamera: + return FileSourceDigitalCamera + default: + return "" + } +} + +type Orientation string + +const ( + OrientationTopLeft Orientation = "top_left" + OrientationTopRight Orientation = "top_right" + OrientationBottomRight Orientation = "bottom_right" + OrientationBottomLeft Orientation = "bottom_left" + OrientationLeftTop Orientation = "left_top" + OrientationRightTop Orientation = "right_top" + OrientationRightBottom Orientation = "right_bottom" + OrientationLeftBottom Orientation = "left_bottom" + + valueOrientationTopLeft = 1 + valueOrientationTopRight = 2 + valueOrientationBottomRight = 3 + valueOrientationBottomLeft = 4 + valueOrientationLeftTop = 5 + valueOrientationRightTop = 6 + valueOrientationRightBottom = 7 + valueOrientationLeftBottom = 8 +) + +func parseOrientation(value *int64) Orientation { + if value == nil { + return "" + } + + switch *value { + case valueOrientationTopLeft: + return OrientationTopLeft + case valueOrientationTopRight: + return OrientationTopRight + case valueOrientationBottomRight: + return OrientationBottomRight + case valueOrientationBottomLeft: + return OrientationBottomLeft + case valueOrientationLeftTop: + return OrientationLeftTop + case valueOrientationRightTop: + return OrientationRightTop + case valueOrientationRightBottom: + return OrientationRightBottom + case valueOrientationLeftBottom: + return OrientationLeftBottom + default: + return "" + } +} + +type ContrastMode string + +const ( + ContrastModeNormal ContrastMode = "normal" + ContrastModeSoft ContrastMode = "soft" + ContrastModeHard ContrastMode = "hard" + + valueContrastModeNormal = 0 + valueContrastModeSoft = 1 + valueContrastModeHard = 2 +) + +func parseContrastMode(value *int64) ContrastMode { + if value == nil { + return "" + } + + switch *value { + case valueContrastModeNormal: + return ContrastModeNormal + case valueContrastModeSoft: + return ContrastModeSoft + case valueContrastModeHard: + return ContrastModeHard + default: + return "" + } +} + +type DistanceRange string + +const ( + DistanceRangeMacro DistanceRange = "macro" + DistanceRangeClose DistanceRange = "close" + DistanceRangeDistant DistanceRange = "distant" + + valueDistanceRangeMacro = 1 + valueDistanceRangeClose = 2 + valueDistanceRangeDistant = 3 +) + +func parseDistanceRange(value *int64) DistanceRange { + if value == nil { + return "" + } + + switch *value { + case valueDistanceRangeMacro: + return DistanceRangeMacro + case valueDistanceRangeClose: + return DistanceRangeClose + case valueDistanceRangeDistant: + return DistanceRangeDistant + default: + return "" + } +} + +type ExposureMode string + +const ( + ExposureModeAuto ExposureMode = "auto" + ExposureModeManual ExposureMode = "manual" + ExposureModeBracket ExposureMode = "bracket" + + valueExposureModeAuto = 0 + valueExposureModeManual = 1 + valueExposureModeBracket = 2 +) + +func parseExposureMode(value *int64) ExposureMode { + if value == nil { + return "" + } + + switch *value { + case valueExposureModeAuto: + return ExposureModeAuto + case valueExposureModeManual: + return ExposureModeManual + case valueExposureModeBracket: + return ExposureModeBracket + default: + return "" + } +} + +type ExposureProgram string + +const ( + ExposureProgramManual ExposureProgram = "manual" + ExposureProgramNormal ExposureProgram = "normal" + ExposureProgramAperturePriority ExposureProgram = "aperture_priority" + ExposureProgramShutterPriority ExposureProgram = "shutter_priority" + ExposureProgramCreative ExposureProgram = "creative" + ExposureProgramAction ExposureProgram = "action" + ExposureProgramPortrait ExposureProgram = "portrait" + ExposureProgramLandscape ExposureProgram = "landscape" + + valueExposureProgramManual = 1 + valueExposureProgramNormal = 2 + valueExposureProgramAperturePriority = 3 + valueExposureProgramShutterPriority = 4 + valueExposureProgramCreative = 5 + valueExposureProgramAction = 6 + valueExposureProgramPortrait = 7 + valueExposureProgramLandscape = 8 +) + +func parseExposureProgram(value *int64) ExposureProgram { + if value == nil { + return "" + } + + switch *value { + case valueExposureProgramManual: + return ExposureProgramManual + case valueExposureProgramNormal: + return ExposureProgramNormal + case valueExposureProgramAperturePriority: + return ExposureProgramAperturePriority + case valueExposureProgramShutterPriority: + return ExposureProgramShutterPriority + case valueExposureProgramCreative: + return ExposureProgramCreative + case valueExposureProgramAction: + return ExposureProgramAction + case valueExposureProgramPortrait: + return ExposureProgramPortrait + case valueExposureProgramLandscape: + return ExposureProgramLandscape + default: + return "" + } +} + +type FlashMode string + +const ( + FlashModeAlwaysOn FlashMode = "always_on" + FlashModeAlwaysOff FlashMode = "always_off" + FlashModeAuto FlashMode = "auto" + + valueFlashModeAlwaysOn = 1 + valueFlashModeAlwaysOff = 2 + valueFlashModeAuto = 3 +) + +func parseFlashMode(value int64) FlashMode { + switch value { + case valueFlashModeAlwaysOn: + return FlashModeAlwaysOn + case valueFlashModeAlwaysOff: + return FlashModeAlwaysOff + case valueFlashModeAuto: + return FlashModeAuto + default: + return "" + } +} + +type LightSource string + +const ( + LightSourceDaylight LightSource = "daylight" + LightSourceFluorescent LightSource = "fluorescent" + LightSourceIncandescent LightSource = "incandescent" + LightSourceFlash LightSource = "flash" + LightSourceFineWeather LightSource = "weather_fine" + LightSourceCloudyWeather LightSource = "weather_cloudy" + LightSourceShade LightSource = "shade" + LightSource6400K LightSource = "6400K" + LightSource5000K LightSource = "5000K" + LightSource4200K LightSource = "4200K" + LightSource3450K LightSource = "3450K" + LightSourceStandardA LightSource = "standard_a" + LightSourceStandardB LightSource = "standard_b" + LightSourceStandardC LightSource = "standard_c" + LightSourceD55 LightSource = "D55" + LightSourceD65 LightSource = "D65" + LightSourceD75 LightSource = "D75" + LightSourceD50 LightSource = "D50" + LightSourceIsoStudio LightSource = "iso_studio" + + valueLightSourceDaylight = 1 + valueLightSourceFluorescent = 2 + valueLightSourceIncandescent = 3 + valueLightSourceFlash = 4 + valueLightSourceFineWeather = 9 + valueLightSourceCloudyWeather = 10 + valueLightSourceShade = 11 + valueLightSource6400K = 12 + valueLightSource5000K = 13 + valueLightSource4200K = 14 + valueLightSource3450K = 15 + valueLightSourceStandardA = 17 + valueLightSourceStandardB = 18 + valueLightSourceStandardC = 19 + valueLightSourceD55 = 20 + valueLightSourceD65 = 21 + valueLightSourceD75 = 22 + valueLightSourceD50 = 23 + valueLightSourceIsoStudio = 24 +) + +func parseLightSource(value *int64) LightSource { + if value == nil { + return "" + } + + switch *value { + case valueLightSourceDaylight: + return LightSourceDaylight + case valueLightSourceFluorescent: + return LightSourceFluorescent + case valueLightSourceIncandescent: + return LightSourceIncandescent + case valueLightSourceFlash: + return LightSourceFlash + case valueLightSourceFineWeather: + return LightSourceFineWeather + case valueLightSourceCloudyWeather: + return LightSourceCloudyWeather + case valueLightSourceShade: + return LightSourceShade + case valueLightSource6400K: + return LightSource6400K + case valueLightSource5000K: + return LightSource5000K + case valueLightSource4200K: + return LightSource4200K + case valueLightSource3450K: + return LightSource3450K + case valueLightSourceStandardA: + return LightSourceStandardA + case valueLightSourceStandardB: + return LightSourceStandardB + case valueLightSourceStandardC: + return LightSourceStandardC + case valueLightSourceD55: + return LightSourceD55 + case valueLightSourceD65: + return LightSourceD65 + case valueLightSourceD75: + return LightSourceD75 + case valueLightSourceD50: + return LightSourceD50 + case valueLightSourceIsoStudio: + return LightSourceIsoStudio + default: + return "" + } +} + +type MeteringMode string + +const ( + MeteringModeAverage MeteringMode = "average" + MeteringModeCenterWeightedAverage MeteringMode = "center_weighted_average" + MeteringModeSpot MeteringMode = "spot" + MeteringModeMultiSpot MeteringMode = "multi_sport" + MeteringModePattern MeteringMode = "pattern" + MeteringModePartial MeteringMode = "partial" + + valueMeteringModeAverage = 1 + valueMeteringModeCenterWeightedAverage = 2 + valueMeteringModeSpot = 3 + valueMeteringModeMultiSpot = 4 + valueMeteringModePattern = 5 + valueMeteringModePartial = 6 +) + +func parseMeteringMode(value *int64) MeteringMode { + if value == nil { + return "" + } + + switch *value { + case valueMeteringModeAverage: + return MeteringModeAverage + case valueMeteringModeCenterWeightedAverage: + return MeteringModeCenterWeightedAverage + case valueMeteringModeSpot: + return MeteringModeSpot + case valueMeteringModeMultiSpot: + return MeteringModeMultiSpot + case valueMeteringModePattern: + return MeteringModePattern + case valueMeteringModePartial: + return MeteringModePartial + default: + return "" + } +} + +type SceneMode string + +const ( + SceneModeStandard SceneMode = "standard" + SceneModeLandscape SceneMode = "landscape" + SceneModePortrait SceneMode = "portrait" + SceneModeNightScene SceneMode = "night" + + valueSceneModeStandard = 0 + valueSceneModeLandscape = 1 + valueSceneModePortrait = 2 + valueSceneModeNightScene = 3 +) + +func parseSceneMode(value *int64) SceneMode { + if value == nil { + return "" + } + + switch *value { + case valueSceneModeStandard: + return SceneModeStandard + case valueSceneModeLandscape: + return SceneModeLandscape + case valueSceneModePortrait: + return SceneModePortrait + case valueSceneModeNightScene: + return SceneModeNightScene + default: + return "" + } +} + +type SharpnessMode string + +const ( + SharpnessModeNormal SharpnessMode = "normal" + SharpnessModeSoft SharpnessMode = "soft" + SharpnessModeHard SharpnessMode = "hard" + + valueSharpnessModeNormal = 0 + valueSharpnessModeSoft = 1 + valueSharpnessModeHard = 2 +) + +func parseSharpnessMode(value *int64) SharpnessMode { + if value == nil { + return "" + } + + switch *value { + case valueSharpnessModeNormal: + return SharpnessModeNormal + case valueSharpnessModeSoft: + return SharpnessModeSoft + case valueSharpnessModeHard: + return SharpnessModeHard + default: + return "" + } +} + +type WhiteBalance string + +const ( + WhiteBalanceAuto WhiteBalance = "auto" + WhiteBalanceManual WhiteBalance = "manual" + + valueWhiteBalanceAuto = 0 + valueWhiteBalanceManual = 1 +) + +func parseWhiteBalance(value *int64) WhiteBalance { + if value == nil { + return "" + } + + switch *value { + case valueWhiteBalanceAuto: + return WhiteBalanceAuto + case valueWhiteBalanceManual: + return WhiteBalanceManual + default: + return "" + } +} diff --git a/imgconv/profiles.go b/imgconv/profiles.go new file mode 100644 index 0000000000000000000000000000000000000000..26c14f671e0b22e3eb26cd976c48039e4f24f540 --- /dev/null +++ b/imgconv/profiles.go @@ -0,0 +1,6 @@ +package imgconv + +import "encoding/base64" + +var ProfileACESLinear, _ = base64.StdEncoding.DecodeString("AAAEMGxjbXMEMAAAbW50clJHQiBYWVogB+AABQABAA0AGQABYWNzcCpuaXgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1sY21zAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMZGVzYwAAARQAAABGY3BydAAAAVwAAAE2d3RwdAAAApQAAAAUY2hhZAAAAqgAAAAsclhZWgAAAtQAAAAUYlhZWgAAAugAAAAUZ1hZWgAAAvwAAAAUclRSQwAAAxAAAAAQZ1RSQwAAAxAAAAAQYlRSQwAAAxAAAAAQY2hybQAAAyAAAAAkZG1uZAAAA0QAAADqbWx1YwAAAAAAAAABAAAADGVuVVMAAAAqAAAAHABBAEMARQBTAC0AZQBsAGwAZQAtAFYANAAtAGcAMQAwAC4AaQBjAGMAAAAAbWx1YwAAAAAAAAABAAAADGVuVVMAAAEaAAAAHABDAG8AcAB5AHIAaQBnAGgAdAAgADIAMAAxADYALAAgAEUAbABsAGUAIABTAHQAbwBuAGUAIAAoAGgAdAB0AHAAOgAvAC8AbgBpAG4AZQBkAGUAZwByAGUAZQBzAGIAZQBsAG8AdwAuAGMAbwBtAC8AKQAsACAAQwBDAC0AQgBZAC0AUwBBACAAMwAuADAAIABVAG4AcABvAHIAdABlAGQAIAAoAGgAdAB0AHAAcwA6AC8ALwBjAHIAZQBhAHQAaQB2AGUAYwBvAG0AbQBvAG4AcwAuAG8AcgBnAC8AbABpAGMAZQBuAHMAZQBzAC8AYgB5AC0AcwBhAC8AMwAuADAALwBsAGUAZwBhAGwAYwBvAGQAZQApAC4AAAAAWFlaIAAAAAAAAPbWAAEAAAAA0y1zZjMyAAAAAAABCL8AAARO///2aAAABYkAAP4D///8vv///jkAAALmAADQIlhZWiAAAAAAAAD9qwAAXKX///9OWFlaIAAAAAD///YJ///qZAAA0cJYWVogAAAAAAAAAyIAALj3AAACHXBhcmEAAAAAAAAAAAABAABjaHJtAAAAAAADAAAAALwWAABD6wAAAAAAAQAAAAAAB///7EltbHVjAAAAAAAAAAEAAAAMZW5VUwAAAM4AAAAcAEEAQwBFAFMAIABjAGgAcgBvAG0AYQB0AGkAYwBpAHQAaQBlAHMAIABmAHIAbwBtACAAVABCAC0AMgAwADEANAAtADAAMAA0ACwAIABoAHQAdABwADoALwAvAHcAdwB3AC4AbwBzAGMAYQByAHMALgBvAHIAZwAvAHMAYwBpAGUAbgBjAGUALQB0AGUAYwBoAG4AbwBsAG8AZwB5AC8AYQBjAGUAcwAvAGEAYwBlAHMALQBkAG8AYwB1AG0AZQBuAHQAYQB0AGkAbwBuAAAAAA==") +var ProfileSRGB, _ = base64.StdEncoding.DecodeString("AAAE6GxjbXMEMAAAbW50clJHQiBYWVogB+AABQABAA0AGQABYWNzcCpuaXgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPbWAAEAAAAA0y1sY21zAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMZGVzYwAAARQAAABOY3BydAAAAWQAAAE2d3RwdAAAApwAAAAUY2hhZAAAArAAAAAsclhZWgAAAtwAAAAUYlhZWgAAAvAAAAAUZ1hZWgAAAwQAAAAUclRSQwAAAxgAAAAgZ1RSQwAAAxgAAAAgYlRSQwAAAxgAAAAgY2hybQAAAzgAAAAkZG1uZAAAA1wAAAGMbWx1YwAAAAAAAAABAAAADGVuVVMAAAAyAAAAHABzAFIARwBCAC0AZQBsAGwAZQAtAFYANAAtAHMAcgBnAGIAdAByAGMALgBpAGMAYwAAAABtbHVjAAAAAAAAAAEAAAAMZW5VUwAAARoAAAAcAEMAbwBwAHkAcgBpAGcAaAB0ACAAMgAwADEANgAsACAARQBsAGwAZQAgAFMAdABvAG4AZQAgACgAaAB0AHQAcAA6AC8ALwBuAGkAbgBlAGQAZQBnAHIAZQBlAHMAYgBlAGwAbwB3AC4AYwBvAG0ALwApACwAIABDAEMALQBCAFkALQBTAEEAIAAzAC4AMAAgAFUAbgBwAG8AcgB0AGUAZAAgACgAaAB0AHQAcABzADoALwAvAGMAcgBlAGEAdABpAHYAZQBjAG8AbQBtAG8AbgBzAC4AbwByAGcALwBsAGkAYwBlAG4AcwBlAHMALwBiAHkALQBzAGEALwAzAC4AMAAvAGwAZQBnAGEAbABjAG8AZABlACkALgAAAABYWVogAAAAAAAA9tYAAQAAAADTLXNmMzIAAAAAAAEMQgAABd7///MlAAAHkwAA/ZD///uh///9ogAAA9wAAMBuWFlaIAAAAAAAAG+gAAA49QAAA5BYWVogAAAAAAAAJJ8AAA+EAAC2xFhZWiAAAAAAAABilwAAt4cAABjZcGFyYQAAAAAAAwAAAAJmZgAA8qcAAA1ZAAAT0AAACltjaHJtAAAAAAADAAAAAKPXAABUfAAATM0AAJmaAAAmZwAAD1xtbHVjAAAAAAAAAAEAAAAMZW5VUwAAAXAAAAAcAHMAUgBHAEIAIABjAGgAcgBvAG0AYQB0AGkAYwBpAHQAaQBlAHMAIABmAHIAbwBtACAAQQAgAFMAdABhAG4AZABhAHIAZAAgAEQAZQBmAGEAdQBsAHQAIABDAG8AbABvAHIAIABTAHAAYQBjAGUAIABmAG8AcgAgAHQAaABlACAASQBuAHQAZQByAG4AZQB0ACAALQAgAHMAUgBHAEIALAAgAGgAdAB0AHAAOgAvAC8AdwB3AHcALgB3ADMALgBvAHIAZwAvAEcAcgBhAHAAaABpAGMAcwAvAEMAbwBsAG8AcgAvAHMAUgBHAEIAOwAgAGEAbABzAG8AIABzAGUAZQAgAGgAdAB0AHAAOgAvAC8AdwB3AHcALgBjAG8AbABvAHIALgBvAHIAZwAvAHMAcABlAGMAaQBmAGkAYwBhAHQAaQBvAG4ALwBJAEMAQwAxAHYANAAzAF8AMgAwADEAMAAtADEAMgAuAHAAZABmAAA=") diff --git a/imgconv/types.go b/imgconv/types.go new file mode 100644 index 0000000000000000000000000000000000000000..64686302170c595c656b12dc95898f322269b9df --- /dev/null +++ b/imgconv/types.go @@ -0,0 +1,32 @@ +package imgconv + +import ( + "gopkg.in/gographics/imagick.v2/imagick" +) + +const ( + ImageFitCover = "cover" + ImageFitContain = "contain" +) + +type Size struct { + Width uint `json:"width" yaml:"width"` + Height uint `json:"height" yaml:"height"` + Format string `json:"format" yaml:"format"` +} + +type Quality struct { + CompressionQuality uint `json:"compression_quality" yaml:"compression-quality"` + SamplingFactors []float64 `json:"sampling_factors" yaml:"sampling-factors"` +} + +type ColorProfile struct { + data []byte + format string +} + +type ImageHandle struct { + wand *imagick.MagickWand + depth uint + profiles []ColorProfile +} diff --git a/shared/config.go b/shared/config.go index 25928b89941c092dd68e9e723249a7155930a1c1..966b014191db480ba0ee2fe128b3363faa5e2136 100644 --- a/shared/config.go +++ b/shared/config.go @@ -1,8 +1,8 @@ package shared import ( + "git.kuschku.de/justjanne/imghost/imgconv" "github.com/hibiken/asynq" - "github.com/justjanne/imgconv" "gopkg.in/yaml.v2" "log" "os" @@ -34,6 +34,7 @@ type Config struct { Database DatabaseConfig `yaml:"database"` Concurrency int `yaml:"concurrency"` UploadTimeout string `yaml:"upload-timeout"` + BaseUrl string `yaml:"base-url"` } func LoadConfigFromFile(file *os.File) Config {