init
This commit is contained in:
@@ -0,0 +1,154 @@
|
||||
package media
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
_ "image/png"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/example/sndit/backend/internal/storage"
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
type Processor struct {
|
||||
repository *Repository
|
||||
storage storage.Storage
|
||||
jobs chan uuid.UUID
|
||||
stop chan struct{}
|
||||
waitGroup sync.WaitGroup
|
||||
}
|
||||
|
||||
func NewProcessor(repository *Repository, objectStorage storage.Storage, workers int) *Processor {
|
||||
if workers < 1 {
|
||||
workers = 1
|
||||
}
|
||||
processor := &Processor{
|
||||
repository: repository,
|
||||
storage: objectStorage,
|
||||
jobs: make(chan uuid.UUID, 256),
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
for index := 0; index < workers; index++ {
|
||||
processor.waitGroup.Add(1)
|
||||
go processor.worker()
|
||||
}
|
||||
return processor
|
||||
}
|
||||
|
||||
func (p *Processor) Enqueue(mediaID uuid.UUID) {
|
||||
select {
|
||||
case p.jobs <- mediaID:
|
||||
case <-p.stop:
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) Close() {
|
||||
close(p.stop)
|
||||
p.waitGroup.Wait()
|
||||
}
|
||||
|
||||
func (p *Processor) worker() {
|
||||
defer p.waitGroup.Done()
|
||||
for {
|
||||
select {
|
||||
case mediaID := <-p.jobs:
|
||||
p.process(mediaID)
|
||||
case <-p.stop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Processor) process(mediaID uuid.UUID) {
|
||||
ctx := context.Background()
|
||||
item, err := p.repository.GetByID(ctx, mediaID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if item.ExternalURL != "" || !IsImage(item) {
|
||||
_ = p.repository.MarkReady(ctx, mediaID, item.StorageKey, item.StorageKey, item.Width, item.Height)
|
||||
return
|
||||
}
|
||||
|
||||
object, err := p.storage.Get(ctx, item.StorageKey)
|
||||
if err != nil {
|
||||
_ = p.repository.MarkFailed(ctx, mediaID, err.Error())
|
||||
return
|
||||
}
|
||||
defer object.Close()
|
||||
|
||||
decoded, _, err := image.Decode(io.LimitReader(object, 100<<20))
|
||||
if err != nil {
|
||||
// Formats without a stdlib decoder, such as HEIC, remain usable through
|
||||
// the original object until a dedicated processing service is added.
|
||||
_ = p.repository.MarkReady(ctx, mediaID, item.StorageKey, item.StorageKey, 0, 0)
|
||||
return
|
||||
}
|
||||
|
||||
width := decoded.Bounds().Dx()
|
||||
height := decoded.Bounds().Dy()
|
||||
previewKey := variantKey(item, "preview.jpg")
|
||||
thumbnailKey := variantKey(item, "thumbnail.jpg")
|
||||
preview, err := encodeJPEG(resize(decoded, 2400), 88)
|
||||
if err != nil {
|
||||
_ = p.repository.MarkFailed(ctx, mediaID, err.Error())
|
||||
return
|
||||
}
|
||||
thumbnail, err := encodeJPEG(resize(decoded, 640), 84)
|
||||
if err != nil {
|
||||
_ = p.repository.MarkFailed(ctx, mediaID, err.Error())
|
||||
return
|
||||
}
|
||||
if err := p.storage.Put(ctx, previewKey, bytes.NewReader(preview), int64(len(preview)), "image/jpeg"); err != nil {
|
||||
_ = p.repository.MarkFailed(ctx, mediaID, err.Error())
|
||||
return
|
||||
}
|
||||
if err := p.storage.Put(ctx, thumbnailKey, bytes.NewReader(thumbnail), int64(len(thumbnail)), "image/jpeg"); err != nil {
|
||||
_ = p.repository.MarkFailed(ctx, mediaID, err.Error())
|
||||
return
|
||||
}
|
||||
if err := p.repository.MarkReady(ctx, mediaID, previewKey, thumbnailKey, width, height); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func variantKey(item Record, filename string) string {
|
||||
return fmt.Sprintf("galleries/%s/%s/%s", item.GalleryID, item.ID, filename)
|
||||
}
|
||||
|
||||
func resize(source image.Image, maxSide int) image.Image {
|
||||
bounds := source.Bounds()
|
||||
width, height := bounds.Dx(), bounds.Dy()
|
||||
if width <= maxSide && height <= maxSide {
|
||||
return source
|
||||
}
|
||||
|
||||
scale := float64(maxSide) / float64(width)
|
||||
if height > width {
|
||||
scale = float64(maxSide) / float64(height)
|
||||
}
|
||||
newWidth := int(float64(width) * scale)
|
||||
newHeight := int(float64(height) * scale)
|
||||
destination := image.NewRGBA(image.Rect(0, 0, newWidth, newHeight))
|
||||
for y := 0; y < newHeight; y++ {
|
||||
for x := 0; x < newWidth; x++ {
|
||||
sourceX := bounds.Min.X + x*width/newWidth
|
||||
sourceY := bounds.Min.Y + y*height/newHeight
|
||||
destination.Set(x, y, source.At(sourceX, sourceY))
|
||||
}
|
||||
}
|
||||
return destination
|
||||
}
|
||||
|
||||
func encodeJPEG(source image.Image, quality int) ([]byte, error) {
|
||||
var output bytes.Buffer
|
||||
if err := jpeg.Encode(&output, source, &jpeg.Options{Quality: quality}); err != nil {
|
||||
return nil, fmt.Errorf("encode preview: %w", err)
|
||||
}
|
||||
return output.Bytes(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user