Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

缩略图生成器支持 LibRaw #2109

Merged
merged 3 commits into from
Jul 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions models/defaults.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ Neue',Helvetica,Arial,sans-serif; box-sizing: border-box; font-size: 14px; verti
{Name: "thumb_proxy_enabled", Value: "0", Type: "thumb"},
{Name: "thumb_proxy_policy", Value: "[]", Type: "thumb"},
{Name: "thumb_max_src_size", Value: "31457280", Type: "thumb"},
{Name: "thumb_libraw_path", Value: "simple_dcraw", Type: "thumb"},
{Name: "thumb_libraw_enabled", Value: "0", Type: "thumb"},
{Name: "thumb_libraw_exts", Value: "arw,raf,dng", Type: "thumb"},
{Name: "pwa_small_icon", Value: "/static/img/favicon.ico", Type: "pwa"},
{Name: "pwa_medium_icon", Value: "/static/img/logo192.png", Type: "pwa"},
{Name: "pwa_large_icon", Value: "/static/img/logo512.png", Type: "pwa"},
Expand Down
1 change: 1 addition & 0 deletions pkg/filesystem/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@ func (fs *FileSystem) generateThumbnail(ctx context.Context, file *model.File) e
"thumb_vips_enabled",
"thumb_ffmpeg_enabled",
"thumb_libreoffice_enabled",
"thumb_libraw_enabled",
))
if err != nil {
_ = updateThumbStatus(file, model.ThumbStatusNotAvailable)
Expand Down
283 changes: 283 additions & 0 deletions pkg/thumb/libraw.go
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)
17 changes: 17 additions & 0 deletions pkg/thumb/tester.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ func TestGenerator(ctx context.Context, name, executable string) (string, error)
return testFfmpegGenerator(ctx, executable)
case "libreOffice":
return testLibreOfficeGenerator(ctx, executable)
case "libRaw":
return testLibRawGenerator(ctx, executable)
default:
return "", ErrUnknownGenerator
}
Expand Down Expand Up @@ -72,3 +74,18 @@ func testLibreOfficeGenerator(ctx context.Context, executable string) (string, e

return output.String(), nil
}

func testLibRawGenerator(ctx context.Context, executable string) (string, error) {
cmd := exec.CommandContext(ctx, executable)
var output bytes.Buffer
cmd.Stdout = &output
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to invoke libraw executable: %w", err)
}

if !strings.Contains(output.String(), "LibRaw") {
return "", ErrUnknownOutput
}

return output.String(), nil
}
Loading