-
-
Notifications
You must be signed in to change notification settings - Fork 3.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* feat: 增加 LibRaw 缩略图生成器。 * feat: 生成 RAW 图像的缩略图时,旋转缩略图的方向。 * update: RAW 缩略图支持镜像方向。
- Loading branch information
Showing
4 changed files
with
304 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,283 @@ | ||
package thumb | ||
|
||
import ( | ||
"bytes" | ||
"context" | ||
"errors" | ||
"fmt" | ||
"image" | ||
"image/jpeg" | ||
"image/png" | ||
"io" | ||
"os" | ||
"os/exec" | ||
"path/filepath" | ||
"strings" | ||
|
||
model "github.com/cloudreve/Cloudreve/v3/models" | ||
"github.com/cloudreve/Cloudreve/v3/pkg/util" | ||
"github.com/gofrs/uuid" | ||
) | ||
|
||
func init() { | ||
RegisterGenerator(&LibRawGenerator{}) | ||
} | ||
|
||
type LibRawGenerator struct { | ||
exts []string | ||
lastRawExts string | ||
} | ||
|
||
func (f *LibRawGenerator) Generate(ctx context.Context, file io.Reader, _ string, name string, options map[string]string) (*Result, error) { | ||
const ( | ||
thumbLibRawPath = "thumb_libraw_path" | ||
thumbLibRawExt = "thumb_libraw_exts" | ||
thumbTempPath = "temp_path" | ||
) | ||
|
||
opts := model.GetSettingByNames(thumbLibRawPath, thumbLibRawExt, thumbTempPath) | ||
|
||
if f.lastRawExts != opts[thumbLibRawExt] { | ||
f.exts = strings.Split(opts[thumbLibRawExt], ",") | ||
f.lastRawExts = opts[thumbLibRawExt] | ||
} | ||
|
||
if !util.IsInExtensionList(f.exts, name) { | ||
return nil, fmt.Errorf("unsupported image format: %w", ErrPassThrough) | ||
} | ||
|
||
inputFilePath := filepath.Join( | ||
util.RelativePath(opts[thumbTempPath]), | ||
"thumb", | ||
fmt.Sprintf("thumb_%s", uuid.Must(uuid.NewV4()).String()), | ||
) | ||
defer func() { _ = os.Remove(inputFilePath) }() | ||
|
||
inputFile, err := util.CreatNestedFile(inputFilePath) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to create temp file: %w", err) | ||
} | ||
|
||
if _, err = io.Copy(inputFile, file); err != nil { | ||
_ = inputFile.Close() | ||
return nil, fmt.Errorf("failed to write input file: %w", err) | ||
} | ||
_ = inputFile.Close() | ||
|
||
cmd := exec.CommandContext(ctx, opts[thumbLibRawPath], "-e", inputFilePath) | ||
|
||
var stdErr bytes.Buffer | ||
cmd.Stderr = &stdErr | ||
if err = cmd.Run(); err != nil { | ||
util.Log().Warning("Failed to invoke LibRaw: %s", stdErr.String()) | ||
return nil, fmt.Errorf("failed to invoke LibRaw: %w", err) | ||
} | ||
|
||
outputFilePath := inputFilePath + ".thumb.jpg" | ||
defer func() { _ = os.Remove(outputFilePath) }() | ||
|
||
ff, err := os.Open(outputFilePath) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to open temp file: %w", err) | ||
} | ||
defer func() { _ = ff.Close() }() | ||
|
||
// use builtin generator | ||
result, err := new(Builtin).Generate(ctx, ff, outputFilePath, filepath.Base(outputFilePath), options) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to generate thumbnail: %w", err) | ||
} | ||
|
||
orientation, err := getJpegOrientation(outputFilePath) | ||
if err != nil { | ||
return nil, fmt.Errorf("failed to get jpeg orientation: %w", err) | ||
} | ||
if orientation == 1 { | ||
return result, nil | ||
} | ||
|
||
if err = rotateImg(result.Path, orientation); err != nil { | ||
return nil, fmt.Errorf("failed to rotate image: %w", err) | ||
} | ||
return result, nil | ||
} | ||
|
||
func rotateImg(filePath string, orientation int) error { | ||
resultImg, err := os.OpenFile(filePath, os.O_RDWR, 0777) | ||
if err != nil { | ||
return err | ||
} | ||
defer func() { _ = resultImg.Close() }() | ||
|
||
imgFlag := make([]byte, 3) | ||
if _, err = io.ReadFull(resultImg, imgFlag); err != nil { | ||
return err | ||
} | ||
if _, err = resultImg.Seek(0, 0); err != nil { | ||
return err | ||
} | ||
|
||
var img image.Image | ||
if bytes.Equal(imgFlag, []byte{0xFF, 0xD8, 0xFF}) { | ||
img, err = jpeg.Decode(resultImg) | ||
} else { | ||
img, err = png.Decode(resultImg) | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
|
||
switch orientation { | ||
case 8: | ||
img = rotate90(img) | ||
case 3: | ||
img = rotate90(rotate90(img)) | ||
case 6: | ||
img = rotate90(rotate90(rotate90(img))) | ||
case 2: | ||
img = mirrorImg(img) | ||
case 7: | ||
img = rotate90(mirrorImg(img)) | ||
case 4: | ||
img = rotate90(rotate90(mirrorImg(img))) | ||
case 5: | ||
img = rotate90(rotate90(rotate90(mirrorImg(img)))) | ||
} | ||
|
||
if err = resultImg.Truncate(0); err != nil { | ||
return err | ||
} | ||
if _, err = resultImg.Seek(0, 0); err != nil { | ||
return err | ||
} | ||
|
||
if bytes.Equal(imgFlag, []byte{0xFF, 0xD8, 0xFF}) { | ||
return jpeg.Encode(resultImg, img, nil) | ||
} | ||
return png.Encode(resultImg, img) | ||
} | ||
|
||
func getJpegOrientation(fileName string) (int, error) { | ||
f, err := os.Open(fileName) | ||
if err != nil { | ||
return 0, err | ||
} | ||
defer func() { _ = f.Close() }() | ||
|
||
header := make([]byte, 6) | ||
defer func() { header = nil }() | ||
if _, err = io.ReadFull(f, header); err != nil { | ||
return 0, err | ||
} | ||
|
||
// jpeg format header | ||
if !bytes.Equal(header[:3], []byte{0xFF, 0xD8, 0xFF}) { | ||
return 0, errors.New("not a jpeg") | ||
} | ||
|
||
// not a APP1 marker | ||
if header[3] != 0xE1 { | ||
return 1, nil | ||
} | ||
|
||
// exif data total length | ||
totalLen := int(header[4])<<8 + int(header[5]) - 2 | ||
buf := make([]byte, totalLen) | ||
defer func() { buf = nil }() | ||
if _, err = io.ReadFull(f, buf); err != nil { | ||
return 0, err | ||
} | ||
|
||
// remove Exif identifier code | ||
buf = buf[6:] | ||
|
||
// byte order | ||
parse16, parse32, err := initParseMethod(buf[:2]) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
// version | ||
_ = buf[2:4] | ||
|
||
// first IFD offset | ||
offset := parse32(buf[4:8]) | ||
|
||
// first DE offset | ||
offset += 2 | ||
buf = buf[offset:] | ||
|
||
const ( | ||
orientationTag = 0x112 | ||
deEntryLength = 12 | ||
) | ||
for len(buf) > deEntryLength { | ||
tag := parse16(buf[:2]) | ||
if tag == orientationTag { | ||
return int(parse32(buf[8:12])), nil | ||
} | ||
buf = buf[deEntryLength:] | ||
} | ||
|
||
return 0, errors.New("orientation not found") | ||
} | ||
|
||
func initParseMethod(buf []byte) (func([]byte) int16, func([]byte) int32, error) { | ||
if bytes.Equal(buf, []byte{0x49, 0x49}) { | ||
return littleEndian16, littleEndian32, nil | ||
} | ||
if bytes.Equal(buf, []byte{0x4D, 0x4D}) { | ||
return bigEndian16, bigEndian32, nil | ||
} | ||
return nil, nil, errors.New("invalid byte order") | ||
} | ||
|
||
func littleEndian16(buf []byte) int16 { | ||
return int16(buf[0]) | int16(buf[1])<<8 | ||
} | ||
|
||
func bigEndian16(buf []byte) int16 { | ||
return int16(buf[1]) | int16(buf[0])<<8 | ||
} | ||
|
||
func littleEndian32(buf []byte) int32 { | ||
return int32(buf[0]) | int32(buf[1])<<8 | int32(buf[2])<<16 | int32(buf[3])<<24 | ||
} | ||
|
||
func bigEndian32(buf []byte) int32 { | ||
return int32(buf[3]) | int32(buf[2])<<8 | int32(buf[1])<<16 | int32(buf[0])<<24 | ||
} | ||
|
||
func rotate90(img image.Image) image.Image { | ||
bounds := img.Bounds() | ||
width, height := bounds.Dx(), bounds.Dy() | ||
newImg := image.NewRGBA(image.Rect(0, 0, height, width)) | ||
for x := 0; x < width; x++ { | ||
for y := 0; y < height; y++ { | ||
newImg.Set(y, width-x-1, img.At(x, y)) | ||
} | ||
} | ||
return newImg | ||
} | ||
|
||
func mirrorImg(img image.Image) image.Image { | ||
bounds := img.Bounds() | ||
width, height := bounds.Dx(), bounds.Dy() | ||
newImg := image.NewRGBA(image.Rect(0, 0, width, height)) | ||
for x := 0; x < width; x++ { | ||
for y := 0; y < height; y++ { | ||
newImg.Set(width-x-1, y, img.At(x, y)) | ||
} | ||
} | ||
return newImg | ||
} | ||
|
||
func (f *LibRawGenerator) Priority() int { | ||
return 250 | ||
} | ||
|
||
func (f *LibRawGenerator) EnableFlag() string { | ||
return "thumb_libraw_enabled" | ||
} | ||
|
||
var _ Generator = (*LibRawGenerator)(nil) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters