diff --git a/api/infra/file_identifier.go b/api/infra/file_identifier.go index d0152c3a5..6934d8591 100644 --- a/api/infra/file_identifier.go +++ b/api/infra/file_identifier.go @@ -106,6 +106,7 @@ func (fi *FileIdentifier) IsImage(path string) bool { ".gif", ".webp", ".tiff", + ".tif", ".bmp", ".ico", ".heif", diff --git a/api/infra/s3.go b/api/infra/s3.go index 87d9d80da..8fd2e9340 100644 --- a/api/infra/s3.go +++ b/api/infra/s3.go @@ -98,7 +98,7 @@ func (mgr *S3Manager) GetObject(objectName string, bucketName string, opts minio var buf bytes.Buffer written, err := io.Copy(io.Writer(&buf), reader) if err != nil { - return nil, nil, nil + return nil, nil, err } return &buf, &written, nil } @@ -113,7 +113,7 @@ func (mgr *S3Manager) GetObjectWithBuffer(objectName string, bucketName string, } written, err := io.Copy(io.Writer(buf), reader) if err != nil { - return nil, nil + return nil, err } return &written, nil } @@ -129,7 +129,7 @@ func (mgr *S3Manager) GetText(objectName string, bucketName string, opts minio.G buf := new(strings.Builder) _, err = io.Copy(buf, reader) if err != nil { - return "", nil + return "", err } return buf.String(), nil } diff --git a/api/service/mosaic_service.go b/api/service/mosaic_service.go index a993c2a94..b1f3c4495 100644 --- a/api/service/mosaic_service.go +++ b/api/service/mosaic_service.go @@ -96,8 +96,8 @@ func (svc *MosaicService) Create(id string, userID string) error { PipelineID: helper.ToPtr(conversion_client.PipelineMosaic), TaskID: task.GetID(), SnapshotID: snapshot.GetID(), - Bucket: snapshot.GetOriginal().Bucket, - Key: snapshot.GetOriginal().Key, + Bucket: snapshot.GetPreview().Bucket, + Key: snapshot.GetPreview().Key, }); err != nil { return err } @@ -129,7 +129,7 @@ func (svc *MosaicService) Delete(id string, userID string) error { if !snapshot.HasMosaic() { return errorpkg.NewMosaicNotFoundError(nil) } - if svc.fileIdent.IsImage(snapshot.GetOriginal().Key) { + if svc.fileIdent.IsImage(snapshot.GetPreview().Key) { task, err := svc.taskSvc.insertAndSync(repo.TaskInsertOptions{ ID: helper.NewID(), Name: "Deleting mosaic.", @@ -149,7 +149,7 @@ func (svc *MosaicService) Delete(id string, userID string) error { go func(task model.Task, snapshot model.Snapshot) { err = svc.mosaicClient.Delete(mosaic_client.MosaicDeleteOptions{ S3Key: filepath.FromSlash(snapshot.GetID()), - S3Bucket: snapshot.GetOriginal().Bucket, + S3Bucket: snapshot.GetPreview().Bucket, }) if err != nil { value := err.Error() @@ -213,7 +213,7 @@ func (svc *MosaicService) GetInfo(id string, userID string) (*MosaicInfo, error) } res, err := svc.mosaicClient.GetMetadata(mosaic_client.MosaicGetMetadataOptions{ S3Key: filepath.FromSlash(snapshot.GetID()), - S3Bucket: snapshot.GetOriginal().Bucket, + S3Bucket: snapshot.GetPreview().Bucket, }) if err != nil { return nil, err @@ -261,7 +261,7 @@ func (svc *MosaicService) DownloadTileBuffer(id string, opts MosaicDownloadTileO } res, err := svc.mosaicClient.DownloadTileBuffer(mosaic_client.MosaicDownloadTileOptions{ S3Key: filepath.FromSlash(snapshot.GetID()), - S3Bucket: snapshot.GetOriginal().Bucket, + S3Bucket: snapshot.GetPreview().Bucket, ZoomLevel: opts.ZoomLevel, Row: opts.Row, Col: opts.Col, diff --git a/conversion/helper/image.go b/conversion/helper/image.go new file mode 100644 index 000000000..7fc06a52d --- /dev/null +++ b/conversion/helper/image.go @@ -0,0 +1,14 @@ +package helper + +func AspectRatio(width, height, originalWidth, originalHeight int) (int, int) { + if width == 0 && height == 0 { + return originalWidth, originalHeight + } + if width == 0 { + width = (height * originalWidth) / originalHeight + } + if height == 0 { + height = (width * originalHeight) / originalWidth + } + return width, height +} diff --git a/conversion/identifier/file_identifier.go b/conversion/identifier/file_identifier.go index fc114bfae..762626078 100644 --- a/conversion/identifier/file_identifier.go +++ b/conversion/identifier/file_identifier.go @@ -104,6 +104,7 @@ func (fi *FileIdentifier) IsImage(path string) bool { ".gif", ".webp", ".tiff", + ".tif", ".bmp", ".ico", ".heif", @@ -125,6 +126,7 @@ func (fi *FileIdentifier) IsNonAlphaChannelImage(path string) bool { ".jpeg", ".gif", ".tiff", + ".tif", ".bmp", } extension := filepath.Ext(path) diff --git a/conversion/identifier/image_identifier.go b/conversion/identifier/image_identifier.go new file mode 100644 index 000000000..fe968ec5a --- /dev/null +++ b/conversion/identifier/image_identifier.go @@ -0,0 +1,32 @@ +package identifier + +import ( + "path/filepath" + "strings" +) + +type ImageIdentifier struct{} + +func NewImageIdentifier() *ImageIdentifier { + return &ImageIdentifier{} +} + +func (ii ImageIdentifier) IsJPEG(path string) bool { + path = strings.ToLower(path) + return filepath.Ext(path) == ".jpg" || + filepath.Ext(path) == ".jpeg" || + filepath.Ext(path) == ".jpe" || + filepath.Ext(path) == ".jfif" || + filepath.Ext(path) == ".jif" +} + +func (ii ImageIdentifier) IsPNG(path string) bool { + path = strings.ToLower(path) + return filepath.Ext(path) == ".png" +} + +func (ii ImageIdentifier) IsTIFF(path string) bool { + path = strings.ToLower(path) + return filepath.Ext(path) == ".tiff" || + filepath.Ext(path) == ".tif" +} diff --git a/conversion/infra/s3.go b/conversion/infra/s3.go index 9d3e44058..e4775d24a 100644 --- a/conversion/infra/s3.go +++ b/conversion/infra/s3.go @@ -128,7 +128,7 @@ func (mgr *S3Manager) GetObject(objectName string, bucketName string, opts minio var buf bytes.Buffer _, err = io.Copy(io.Writer(&buf), reader) if err != nil { - return nil, nil + return nil, err } return &buf, nil } @@ -146,7 +146,7 @@ func (mgr *S3Manager) GetText(objectName string, bucketName string, opts minio.G buf := new(strings.Builder) _, err = io.Copy(buf, reader) if err != nil { - return "", nil + return "", err } return buf.String(), nil } diff --git a/conversion/pipeline/image_pipeline.go b/conversion/pipeline/image_pipeline.go index 2f5992fa3..3562bdcd5 100644 --- a/conversion/pipeline/image_pipeline.go +++ b/conversion/pipeline/image_pipeline.go @@ -32,6 +32,7 @@ type imagePipeline struct { taskClient *api_client.TaskClient snapshotClient *api_client.SnapshotClient fileIdent *identifier.FileIdentifier + imageIdent *identifier.ImageIdentifier config *config.Config } @@ -42,6 +43,7 @@ func NewImagePipeline() model.Pipeline { taskClient: api_client.NewTaskClient(), snapshotClient: api_client.NewSnapshotClient(), fileIdent: identifier.NewFileIdentifier(), + imageIdent: identifier.NewImageIdentifier(), config: config.GetConfig(), } } @@ -69,7 +71,7 @@ func (p *imagePipeline) Run(opts api_client.PipelineRunOptions) error { return err } var imagePath string - if filepath.Ext(inputPath) == ".tiff" { + if p.imageIdent.IsTIFF(inputPath) { if err := p.taskClient.Patch(opts.TaskID, api_client.TaskPatchOptions{ Fields: []string{api_client.TaskFieldName}, Name: helper.ToPtr("Converting TIFF image to JPEG format."), @@ -137,7 +139,7 @@ func (p *imagePipeline) measureImageDimensions(inputPath string, opts api_client } func (p *imagePipeline) createThumbnail(inputPath string, opts api_client.PipelineRunOptions) error { - tmpPath := filepath.FromSlash(os.TempDir() + "/" + helper.NewID() + ".png") + tmpPath := filepath.FromSlash(os.TempDir() + "/" + helper.NewID() + filepath.Ext(inputPath)) isAvailable, err := p.imageProc.Thumbnail(inputPath, tmpPath) if err != nil { return err diff --git a/conversion/processor/image_processor.go b/conversion/processor/image_processor.go index a42ae3ee1..36be786f1 100644 --- a/conversion/processor/image_processor.go +++ b/conversion/processor/image_processor.go @@ -11,7 +11,6 @@ package processor import ( - "fmt" "strconv" "strings" @@ -26,14 +25,16 @@ import ( ) type ImageProcessor struct { - fileIdent *identifier.FileIdentifier - config *config.Config + fileIdent *identifier.FileIdentifier + imageIdent *identifier.ImageIdentifier + config *config.Config } func NewImageProcessor() *ImageProcessor { return &ImageProcessor{ - fileIdent: identifier.NewFileIdentifier(), - config: config.GetConfig(), + fileIdent: identifier.NewFileIdentifier(), + imageIdent: identifier.NewImageIdentifier(), + config: config.GetConfig(), } } @@ -44,11 +45,13 @@ func (p *ImageProcessor) Thumbnail(inputPath string, outputPath string) (*bool, } if props.Width > p.config.Limits.ImagePreviewMaxWidth || props.Height > p.config.Limits.ImagePreviewMaxHeight { if props.Width > props.Height { - if err := p.ResizeImage(inputPath, p.config.Limits.ImagePreviewMaxWidth, 0, outputPath); err != nil { + newWidth, newHeight := helper.AspectRatio(p.config.Limits.ImagePreviewMaxWidth, 0, props.Width, props.Height) + if err := p.ResizeImage(inputPath, newWidth, newHeight, outputPath); err != nil { return nil, err } } else { - if err := p.ResizeImage(inputPath, 0, p.config.Limits.ImagePreviewMaxHeight, outputPath); err != nil { + newWidth, newHeight := helper.AspectRatio(0, p.config.Limits.ImagePreviewMaxHeight, props.Width, props.Height) + if err := p.ResizeImage(inputPath, newWidth, newHeight, outputPath); err != nil { return nil, err } } @@ -58,55 +61,92 @@ func (p *ImageProcessor) Thumbnail(inputPath string, outputPath string) (*bool, } func (p *ImageProcessor) MeasureImage(inputPath string) (*api_client.ImageProps, error) { - img, err := imgio.Open(inputPath) - if err != nil { - return nil, err + bildImage, err := imgio.Open(inputPath) + if err == nil { + return &api_client.ImageProps{ + Width: bildImage.Bounds().Dx(), + Height: bildImage.Bounds().Dy(), + }, nil + } else { + size, err := infra.NewCommand().ReadOutput("identify", "-format", "%w,%h", inputPath) + if err != nil { + return nil, err + } + values := strings.Split(*size, ",") + width, err := strconv.Atoi(helper.RemoveNonNumeric(values[0])) + if err != nil { + return nil, err + } + height, err := strconv.Atoi(helper.RemoveNonNumeric(values[1])) + if err != nil { + return nil, err + } + return &api_client.ImageProps{ + Width: width, + Height: height, + }, nil } - return &api_client.ImageProps{ - Width: img.Bounds().Dx(), - Height: img.Bounds().Dy(), - }, nil } func (p *ImageProcessor) ResizeImage(inputPath string, width int, height int, outputPath string) error { - img, err := imgio.Open(inputPath) - if err != nil { - return err - } - newImg := transform.Resize(img, width, height, transform.Lanczos) - var encoder imgio.Encoder - if strings.HasSuffix(inputPath, ".png") { - encoder = imgio.PNGEncoder() - } else if strings.HasSuffix(inputPath, ".jpg") { - encoder = imgio.JPEGEncoder(100) + bildImage, err := imgio.Open(inputPath) + if err == nil && p.canBeHandledByBild(outputPath) { + newImage := transform.Resize(bildImage, width, height, transform.Lanczos) + var encoder imgio.Encoder + if p.imageIdent.IsPNG(inputPath) { + encoder = imgio.PNGEncoder() + } else if p.imageIdent.IsJPEG(inputPath) { + encoder = imgio.JPEGEncoder(100) + } + return imgio.Save(outputPath, newImage, encoder) } else { - return fmt.Errorf("unsupported image format: %s", inputPath) + var widthStr string + if width == 0 { + widthStr = "" + } else { + widthStr = strconv.FormatInt(int64(width), 10) + } + var heightStr string + if height == 0 { + heightStr = "" + } else { + heightStr = strconv.FormatInt(int64(height), 10) + } + if err := infra.NewCommand().Exec("convert", "-resize", widthStr+"x"+heightStr, inputPath, outputPath); err != nil { + return err + } + return nil } - return imgio.Save(outputPath, newImg, encoder) } func (p *ImageProcessor) ConvertImage(inputPath string, outputPath string) error { - img, err := imgio.Open(inputPath) - if err != nil { - return err - } - var encoder imgio.Encoder - if strings.HasSuffix(outputPath, ".png") { - encoder = imgio.PNGEncoder() - } else if strings.HasSuffix(outputPath, ".jpg") { - encoder = imgio.JPEGEncoder(100) + bildImage, err := imgio.Open(inputPath) + if err == nil && p.canBeHandledByBild(outputPath) { + var encoder imgio.Encoder + if p.imageIdent.IsPNG(outputPath) { + encoder = imgio.PNGEncoder() + } else if p.imageIdent.IsJPEG(outputPath) { + encoder = imgio.JPEGEncoder(100) + } + return imgio.Save(outputPath, bildImage, encoder) } else { - return fmt.Errorf("unsupported image format: %s", inputPath) + if err := infra.NewCommand().Exec("convert", inputPath, outputPath); err != nil { + return err + } + return nil } - return imgio.Save(outputPath, img, encoder) } func (p *ImageProcessor) RemoveAlphaChannel(inputPath string, outputPath string) error { - img, err := imgio.Open(inputPath) - if err != nil { - return err + bildImage, err := imgio.Open(inputPath) + if err == nil && p.canBeHandledByBild(outputPath) { + return imgio.Save(outputPath, bildImage, imgio.JPEGEncoder(100)) + } else { + if err := infra.NewCommand().Exec("convert", inputPath, "-alpha", "off", outputPath); err != nil { + return err + } + return nil } - return imgio.Save(outputPath, img, imgio.JPEGEncoder(100)) } func (p *ImageProcessor) DPIFromImage(inputPath string) (*int, error) { @@ -128,3 +168,7 @@ func (p *ImageProcessor) DPIFromImage(inputPath string) (*int, error) { } return helper.ToPtr(int((xRes + yRes) / 2)), nil } + +func (p *ImageProcessor) canBeHandledByBild(path string) bool { + return p.imageIdent.IsJPEG(path) || p.imageIdent.IsPNG(path) +} diff --git a/mosaic/infra/s3.go b/mosaic/infra/s3.go index c6a4b02b6..7718f078a 100644 --- a/mosaic/infra/s3.go +++ b/mosaic/infra/s3.go @@ -94,7 +94,7 @@ func (mgr *S3Manager) GetObject(objectName string, bucketName string, opts minio var buf bytes.Buffer _, err = io.Copy(io.Writer(&buf), reader) if err != nil { - return nil, nil + return nil, err } return &buf, nil } @@ -109,7 +109,7 @@ func (mgr *S3Manager) GetObjectWithBuffer(objectName string, bucketName string, } written, err := io.Copy(io.Writer(buf), reader) if err != nil { - return nil, nil + return nil, err } return &written, nil } @@ -127,7 +127,7 @@ func (mgr *S3Manager) GetText(objectName string, bucketName string, opts minio.G buf := new(strings.Builder) _, err = io.Copy(buf, reader) if err != nil { - return "", nil + return "", err } return buf.String(), nil } diff --git a/ui/src/lib/helpers/file-extension.ts b/ui/src/lib/helpers/file-extension.ts index 3921de838..f6be1bf3d 100644 --- a/ui/src/lib/helpers/file-extension.ts +++ b/ui/src/lib/helpers/file-extension.ts @@ -29,6 +29,7 @@ export function isImage(ext?: string | null) { '.gif', '.webp', '.tiff', + '.tif', '.bmp', '.ico', '.heif', diff --git a/webdav/infra/s3.go b/webdav/infra/s3.go index e1c477899..ed4134406 100644 --- a/webdav/infra/s3.go +++ b/webdav/infra/s3.go @@ -97,7 +97,7 @@ func (mgr *S3Manager) GetObject(objectName string, bucketName string, opts minio var buf bytes.Buffer written, err := io.Copy(io.Writer(&buf), reader) if err != nil { - return nil, nil, nil + return nil, nil, err } return &buf, &written, nil } @@ -112,7 +112,7 @@ func (mgr *S3Manager) GetObjectWithBuffer(objectName string, bucketName string, } written, err := io.Copy(io.Writer(buf), reader) if err != nil { - return nil, nil + return nil, err } return &written, nil } @@ -128,7 +128,7 @@ func (mgr *S3Manager) GetText(objectName string, bucketName string, opts minio.G buf := new(strings.Builder) _, err = io.Copy(buf, reader) if err != nil { - return "", nil + return "", err } return buf.String(), nil }