From 95dc125712a664c168e0b17d2a23f0891df34444 Mon Sep 17 00:00:00 2001 From: Arthur Jamet <60505370+Arthi-chaud@users.noreply.github.com> Date: Wed, 25 Dec 2024 17:00:24 +0000 Subject: [PATCH] Scanner: Remove black bars on thumbnails (#810) * Scanner: Crop black bars on thumbnails * Docker: Scanner: Move to alpine to get a newer ffmpeg version --- scanner/Dockerfile | 9 +- scanner/Dockerfile.dev | 5 +- scanner/internal/illustration/thumbnail.go | 111 +++++++++++++++++++-- 3 files changed, 110 insertions(+), 15 deletions(-) diff --git a/scanner/Dockerfile b/scanner/Dockerfile index 5aa3ffa96..d16c663c7 100644 --- a/scanner/Dockerfile +++ b/scanner/Dockerfile @@ -1,6 +1,5 @@ -FROM golang:1.22.6-bullseye AS builder +FROM golang:1.22.6-alpine AS builder RUN go install github.com/swaggo/swag/cmd/swag@latest -RUN apt-get update -y; apt-get install -y ffmpeg WORKDIR /app COPY go.mod go.sum ./ RUN go mod download @@ -9,12 +8,12 @@ COPY ./internal ./internal RUN swag init -d app -o ./app/docs RUN GOOS=linux go build -o ./scanner ./app -FROM debian:bullseye-slim AS runner +FROM golang:1.22.6-alpine AS runner ENV SERVICE_NAME="scanner" -RUN useradd -ms /bin/false $SERVICE_NAME +RUN adduser --disabled-password -s /bin/false $SERVICE_NAME -RUN apt-get update -y; apt-get install -y ffmpeg wget libchromaprint-tools libchromaprint-dev libfftw3-dev +RUN apk update && apk upgrade && apk add ffmpeg chromaprint mailcap WORKDIR /app COPY --from=builder /app/scanner ./ USER $SERVICE_NAME diff --git a/scanner/Dockerfile.dev b/scanner/Dockerfile.dev index 787de85ab..3f254dbec 100644 --- a/scanner/Dockerfile.dev +++ b/scanner/Dockerfile.dev @@ -1,7 +1,8 @@ -FROM golang:1.22.6-bullseye +FROM golang:1.22.6-alpine RUN go install github.com/bokwoon95/wgo@latest RUN go install github.com/swaggo/swag/cmd/swag@latest -RUN apt-get update -y; apt-get install -y ffmpeg libchromaprint-tools libchromaprint-dev libfftw3-dev + +RUN apk update && apk upgrade && apk add ffmpeg chromaprint mailcap WORKDIR /app CMD ["wgo", "-xdir", "./app/docs", "swag", "init", "-d", "app", "-o", "./app/docs", "::", "go", "run", "./app"] diff --git a/scanner/internal/illustration/thumbnail.go b/scanner/internal/illustration/thumbnail.go index 1a5a56724..b31858971 100644 --- a/scanner/internal/illustration/thumbnail.go +++ b/scanner/internal/illustration/thumbnail.go @@ -3,18 +3,113 @@ package illustration import ( "bytes" "fmt" - + "github.com/kpango/glg" "github.com/u2takey/ffmpeg-go" + "strings" ) +type CropDimensions struct { + width int + height int + x int + y int +} + func GetFrame(filepath string, timestamp int64) ([]byte, error) { formattedDuration := fmt.Sprintf("%.2d:%.2d:%.2d", int(timestamp/3600), (timestamp/60)%60, timestamp%60) - buf := bytes.NewBuffer(nil) - err := ffmpeg_go.Input(filepath, ffmpeg_go.KwArgs{"ss": formattedDuration}). + thumbnail := bytes.NewBuffer(nil) + filters := []string{ + "scale='max(iw,iw*sar)':'max(ih,ih/sar)'", + "select=gte(n\\,1)", + } + + cmd := ffmpeg_go.Input(filepath, ffmpeg_go.KwArgs{"ss": formattedDuration}). + Silent(true). + Output("pipe:", ffmpeg_go.KwArgs{ + "vframes": 1, + "format": "image2", + "vcodec": "mjpeg", + "vf": strings.Join(filters, ", ")}). + WithOutput(thumbnail) + err := cmd.Run() + if err != nil { + glg.Error(err) + return nil, err + } + thumbnailBytes := thumbnail.Bytes() + croppedThumbnail, _ := RemoveBlackBars(&thumbnailBytes) + if croppedThumbnail != nil { + return croppedThumbnail.Bytes(), nil + } + return thumbnailBytes, err +} + +func RemoveBlackBars(frame *[]byte) (*bytes.Buffer, error) { + + crops, err := GetCropDimensions(bytes.NewBuffer(*frame)) + if err != nil || crops == nil { + if err != nil { + glg.Error(err) + } + return nil, err + } + newFrame := bytes.NewBuffer(nil) + err = ffmpeg_go.Input("pipe:", ffmpeg_go.KwArgs{"format": "image2pipe"}). + Filter("crop", ffmpeg_go.Args{fmt.Sprintf("%d:%d:%d:%d", crops.width, crops.height, crops.x, crops.y)}). + Output("pipe:", ffmpeg_go.KwArgs{ + "format": "image2", + "vframes": "1", + "vcodec": "mjpeg"}). + WithInput(bytes.NewBuffer(*frame)). + WithOutput(newFrame).Run() + if err != nil { + glg.Error(err) + return nil, err + } + return newFrame, err +} + +func GetCropDimensions(thumbnail *bytes.Buffer) (*CropDimensions, error) { + + rawout := bytes.NewBuffer(nil) + + err := ffmpeg_go.Input("pipe:", ffmpeg_go.KwArgs{"f": "image2pipe", "loop": "1"}). Silent(true). - Filter("scale", ffmpeg_go.Args{"'max(iw,iw*sar)':'max(ih,ih/sar)'"}). - Filter("select", ffmpeg_go.Args{"gte(n,1)"}). - Output("pipe:", ffmpeg_go.KwArgs{"vframes": 1, "format": "image2", "vcodec": "mjpeg"}). - WithOutput(buf).Run() - return buf.Bytes(), err + Output("pipe:", ffmpeg_go.KwArgs{ + "f": "null", + "frames:v": "3", + "vf": "cropdetect=limit=0:round=0"}). + WithErrorOutput(rawout). + WithInput(thumbnail). + WithOutput(rawout).Run() + if err != nil { + glg.Error(err) + return nil, err + } + strout := string((*rawout).Bytes()) + lines := strings.Split(strout, "\n") + lineCount := len(lines) + if lineCount == 0 { + return nil, nil + } + for index := lineCount; index > 0; index-- { + token := "crop=" + line := lines[index-1] + cropPos := strings.Index(line, token) + if cropPos == -1 { + continue + } + dims := CropDimensions{} + _, err = fmt.Sscanf(line[cropPos+len(token):], "%d:%d:%d:%d", + &dims.width, &dims.height, &dims.x, &dims.y) + if err != nil { + glg.Error(err) + continue + } + if dims.width < 0 || dims.height < 0 { + continue + } + return &dims, nil + } + return nil, nil }