110 lines
2.8 KiB
Go
110 lines
2.8 KiB
Go
package thumb
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"image"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"syscall"
|
|
|
|
"github.com/disintegration/imaging"
|
|
"github.com/mandykoh/prism/meta"
|
|
"github.com/mandykoh/prism/meta/autometa"
|
|
"golang.org/x/sys/unix"
|
|
|
|
"github.com/photoprism/photoprism/pkg/clean"
|
|
"github.com/photoprism/photoprism/pkg/colors"
|
|
)
|
|
|
|
var (
|
|
EOI = []byte{0xff, 0xd9}
|
|
)
|
|
|
|
func decode(reader io.Reader, logName string) (md *meta.Data, img image.Image, err error) {
|
|
// Read color metadata.
|
|
md, imgStream, err := autometa.Load(reader)
|
|
|
|
if err != nil {
|
|
log.Warnf("thumb: %s in %s (read color metadata)", err, logName)
|
|
img, err = imaging.Decode(reader)
|
|
} else {
|
|
img, err = imaging.Decode(imgStream)
|
|
}
|
|
return md, img, err
|
|
}
|
|
|
|
func attemptRepair(fileReader *os.File) (io.Reader, error) {
|
|
fi, err := fileReader.Stat()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s trying to stat() file", err)
|
|
}
|
|
size := int(fi.Size())
|
|
b, err := unix.Mmap(int(fileReader.Fd()), 0, size, syscall.PROT_READ|syscall.PROT_WRITE, syscall.MAP_PRIVATE)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s while mmap()ing file", err)
|
|
}
|
|
|
|
// Check for missing EOI.
|
|
if !bytes.Equal(b[size-len(EOI):size], EOI) {
|
|
b = append(b, EOI...)
|
|
}
|
|
|
|
return bytes.NewReader(b), nil
|
|
}
|
|
|
|
// OpenJpeg loads a JPEG image from disk, rotates it, and converts the color profile if necessary.
|
|
func OpenJpeg(fileName string, orientation int) (image.Image, error) {
|
|
if fileName == "" {
|
|
return nil, fmt.Errorf("filename missing")
|
|
}
|
|
|
|
logName := clean.Log(filepath.Base(fileName))
|
|
|
|
// Open file.
|
|
fileReader, err := os.Open(fileName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer fileReader.Close()
|
|
|
|
// Reset file offset.
|
|
// see https://github.com/golang/go/issues/45902#issuecomment-1007953723
|
|
if _, err := fileReader.Seek(0, 0); err != nil {
|
|
return nil, fmt.Errorf("%s on seek", err)
|
|
}
|
|
|
|
md, img, err := decode(fileReader, logName)
|
|
if err != nil {
|
|
log.Warnf("%s during initial decoding attempt", err)
|
|
repaired, err := attemptRepair(fileReader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s while trying to recover image", err)
|
|
}
|
|
if md, img, err = decode(repaired, logName); err != nil {
|
|
return nil, fmt.Errorf("%s while decoding after recovery attempt", err)
|
|
}
|
|
}
|
|
|
|
// Read ICC profile and convert colors if possible.
|
|
if md != nil {
|
|
if iccProfile, err := md.ICCProfile(); err != nil || iccProfile == nil {
|
|
// Do nothing.
|
|
log.Tracef("thumb: %s has no color profile", logName)
|
|
} else if profile, err := iccProfile.Description(); err == nil && profile != "" {
|
|
log.Tracef("thumb: %s has color profile %s", logName, clean.Log(profile))
|
|
switch {
|
|
case colors.ProfileDisplayP3.Equal(profile):
|
|
img = colors.ToSRGB(img, colors.ProfileDisplayP3)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Adjust orientation.
|
|
if orientation > 1 {
|
|
img = Rotate(img, orientation)
|
|
}
|
|
|
|
return img, nil
|
|
}
|