소스 검색

Add imagelib.rawToJPEG AGI function to convert RAW photos to JPEG (#244)

Alan Yeung 1 주 전
부모
커밋
ade2d4918b
4개의 변경된 파일217개의 추가작업 그리고 0개의 파일을 삭제
  1. 15 0
      src/agi-doc.md
  2. 67 0
      src/mod/agi/agi.image.go
  3. 111 0
      src/mod/agi/agi.image_test.go
  4. 24 0
      src/web/UnitTest/backend/imagelib.rawToJPEG.js

+ 15 - 0
src/agi-doc.md

@@ -599,10 +599,25 @@ imagelib.getImageDimension("user:/Desktop/test.jpg");
 imagelib.resizeImage("user:/Desktop/input.png", "user:/Desktop/output.png", 500, 300);     //Resize input.png to 500 x 300 pixal and write to output.png
 imagelib.loadThumbString("user:/Desktop/test.jpg"); //Load the given file's thumbnail as base64 string, return false if failed
 imagelib.cropImage("user:/Desktop/test.jpg", "user:/Desktop/out.jpg",100,100,200,200)); 
+//Convert a RAW photo to a normal JPEG, return true on success and false on failure
+imagelib.rawToJPEG("user:/Desktop/photo.ARW", "user:/Desktop/photo.jpg");
 //Classify an image using neural network, since v1.119
 imagelib.classify("tmp:/classify.jpg", "yolo3"); 
 ```
 
+#### Convert RAW to JPEG
+
+```
+Convert a camera RAW photo into a standard JPEG by extracting its embedded
+full-size preview. Supported RAW formats: .arw, .cr2, .dng, .nef, .raf, .orf
+
+1) Input file (virtual path to the RAW photo)
+2) Output file (virtual path, must end in .jpg or .jpeg, overwritten if exists)
+
+return true if success, false if failed (e.g. input is not a supported RAW
+format, or no embedded preview could be extracted)
+```
+
 #### Crop Image Options
 
 ```

+ 67 - 0
src/mod/agi/agi.image.go

@@ -22,7 +22,9 @@ import (
 	"github.com/rwcarlsen/goexif/exif"
 
 	"imuslab.com/arozos/mod/agi/static"
+	"imuslab.com/arozos/mod/filesystem"
 	"imuslab.com/arozos/mod/filesystem/arozfs"
+	metadata "imuslab.com/arozos/mod/filesystem/metadata"
 	"imuslab.com/arozos/mod/info/logger"
 	"imuslab.com/arozos/mod/utils"
 )
@@ -599,6 +601,46 @@ func (g *Gateway) injectImageLibFunctions(payload *static.AgiLibInjectionPayload
 		return result
 	})
 
+	//Convert a RAW photo (ARW/CR2/DNG/NEF/RAF/ORF) to a normal JPEG, require (input, output)
+	vm.Set("_imagelib_rawToJPEG", func(call otto.FunctionCall) otto.Value {
+		vsrc, err := call.Argument(0).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		vdest, err := call.Argument(1).ToString()
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		//Convert the virtual paths to real paths
+		srcfsh, rsrc, err := static.VirtualPathToRealPath(vsrc, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+		destfsh, rdest, err := static.VirtualPathToRealPath(vdest, u)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		if !srcfsh.FileSystemAbstraction.FileExists(rsrc) {
+			g.RaiseError(errors.New("File not exists! Given " + rsrc))
+			return otto.FalseValue()
+		}
+
+		err = convertRawToJPEG(srcfsh, rsrc, destfsh, rdest)
+		if err != nil {
+			g.RaiseError(err)
+			return otto.FalseValue()
+		}
+
+		return otto.TrueValue()
+	})
+
 	//Wrap all the native code function into an imagelib class
 	vm.Run(`
 		var imagelib = {};
@@ -609,5 +651,30 @@ func (g *Gateway) injectImageLibFunctions(payload *static.AgiLibInjectionPayload
 		imagelib.loadThumbString = _imagelib_loadThumbString;
 		imagelib.hasExif = _imagelib_hasExif;
 		imagelib.getExif = _imagelib_getExif;
+		imagelib.rawToJPEG = _imagelib_rawToJPEG;
 	`)
 }
+
+// convertRawToJPEG renders a RAW photo (ARW, CR2, DNG, NEF, RAF, ORF) into a
+// standard JPEG by extracting its embedded full-size preview, then writes the
+// result to destRpath on destFsh. It returns an error when the source is not a
+// supported RAW format, when the output is not a .jpg/.jpeg file, or when any
+// read / render / write step fails. WriteFile is used for the output so this
+// works on every filesystem abstraction, including buffered ones (S3, FTP, WebDAV).
+func convertRawToJPEG(srcFsh *filesystem.FileSystemHandler, srcRpath string, destFsh *filesystem.FileSystemHandler, destRpath string) error {
+	if !metadata.IsRawImageFile(srcRpath) {
+		return errors.New("source is not a supported RAW image: " + filepath.Base(srcRpath))
+	}
+
+	destExt := strings.ToLower(filepath.Ext(destRpath))
+	if destExt != ".jpg" && destExt != ".jpeg" {
+		return errors.New("output file must have a .jpg or .jpeg extension")
+	}
+
+	jpegData, err := metadata.RenderRAWImage(srcFsh, srcRpath)
+	if err != nil {
+		return err
+	}
+
+	return destFsh.FileSystemAbstraction.WriteFile(destRpath, jpegData, 0775)
+}

+ 111 - 0
src/mod/agi/agi.image_test.go

@@ -0,0 +1,111 @@
+package agi
+
+import (
+	"bytes"
+	"image"
+	"image/jpeg"
+	"os"
+	"path/filepath"
+	"testing"
+	"time"
+
+	"imuslab.com/arozos/mod/filesystem"
+	"imuslab.com/arozos/mod/filesystem/abstractions/localfs"
+)
+
+// newImageTestFSH creates a FileSystemHandler backed by a temporary local
+// directory for exercising the imagelib helpers.
+func newImageTestFSH(t *testing.T) (*filesystem.FileSystemHandler, string) {
+	t.Helper()
+	dir := t.TempDir()
+	abs := localfs.NewLocalFileSystemAbstraction("TEST", dir+"/", "public", false)
+	fsh := &filesystem.FileSystemHandler{
+		Name:                  "test",
+		UUID:                  "TEST",
+		Path:                  dir + "/",
+		ReadOnly:              false,
+		Hierarchy:             "public",
+		InitiationTime:        time.Now().Unix(),
+		FileSystemAbstraction: abs,
+		Filesystem:            "ext4",
+	}
+	return fsh, dir
+}
+
+// smallJPEGBytes returns a small solid image encoded as JPEG. Written into a
+// RAW-extension file it stands in for the embedded preview that real RAW photos
+// carry, which is what convertRawToJPEG extracts.
+func smallJPEGBytes(t *testing.T) []byte {
+	t.Helper()
+	img := image.NewRGBA(image.Rect(0, 0, 32, 24))
+	var buf bytes.Buffer
+	if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 90}); err != nil {
+		t.Fatalf("jpeg encode: %v", err)
+	}
+	return buf.Bytes()
+}
+
+func TestConvertRawToJPEG(t *testing.T) {
+	jpegBytes := smallJPEGBytes(t)
+
+	t.Run("RAW with embedded JPEG is converted", func(t *testing.T) {
+		fsh, dir := newImageTestFSH(t)
+		src := filepath.Join(dir, "photo.arw")
+		if err := os.WriteFile(src, jpegBytes, 0644); err != nil {
+			t.Fatalf("write src: %v", err)
+		}
+		dst := filepath.Join(dir, "photo.jpg")
+
+		if err := convertRawToJPEG(fsh, src, fsh, dst); err != nil {
+			t.Fatalf("convertRawToJPEG returned error: %v", err)
+		}
+
+		out, err := os.ReadFile(dst)
+		if err != nil {
+			t.Fatalf("output not written: %v", err)
+		}
+		img, format, err := image.Decode(bytes.NewReader(out))
+		if err != nil {
+			t.Fatalf("output is not a decodable image: %v", err)
+		}
+		if format != "jpeg" {
+			t.Errorf("expected jpeg output, got %q", format)
+		}
+		if b := img.Bounds(); b.Dx() != 32 || b.Dy() != 24 {
+			t.Errorf("unexpected output dimensions: %dx%d", b.Dx(), b.Dy())
+		}
+	})
+
+	t.Run("non-RAW source is rejected", func(t *testing.T) {
+		fsh, dir := newImageTestFSH(t)
+		src := filepath.Join(dir, "photo.png")
+		if err := os.WriteFile(src, jpegBytes, 0644); err != nil {
+			t.Fatalf("write src: %v", err)
+		}
+		if err := convertRawToJPEG(fsh, src, fsh, filepath.Join(dir, "out.jpg")); err == nil {
+			t.Errorf("expected error for non-RAW source, got nil")
+		}
+	})
+
+	t.Run("non-JPEG output extension is rejected", func(t *testing.T) {
+		fsh, dir := newImageTestFSH(t)
+		src := filepath.Join(dir, "photo.arw")
+		if err := os.WriteFile(src, jpegBytes, 0644); err != nil {
+			t.Fatalf("write src: %v", err)
+		}
+		if err := convertRawToJPEG(fsh, src, fsh, filepath.Join(dir, "out.png")); err == nil {
+			t.Errorf("expected error for non-JPEG output, got nil")
+		}
+	})
+
+	t.Run("RAW without embedded JPEG fails cleanly", func(t *testing.T) {
+		fsh, dir := newImageTestFSH(t)
+		src := filepath.Join(dir, "garbage.cr2")
+		if err := os.WriteFile(src, []byte("not a real raw file"), 0644); err != nil {
+			t.Fatalf("write src: %v", err)
+		}
+		if err := convertRawToJPEG(fsh, src, fsh, filepath.Join(dir, "out.jpg")); err == nil {
+			t.Errorf("expected error for RAW without embedded JPEG, got nil")
+		}
+	})
+}

+ 24 - 0
src/web/UnitTest/backend/imagelib.rawToJPEG.js

@@ -0,0 +1,24 @@
+console.log("RAW to JPEG Conversion Test");
+//To test this, put a RAW photo (e.g. test.ARW / test.CR2 / test.DNG) on your desktop
+var srcPath = "user:/Desktop/test.ARW";
+var destPath = "user:/Desktop/test.jpg";
+
+//Check if the file exists
+requirelib("filelib");
+if (!filelib.fileExists(srcPath)){
+	sendResp("File not exists!")
+}else{
+	//Require the image library
+	var loaded = requirelib("imagelib");
+	if (loaded) {
+		//Library loaded. Call to the functions
+		var success = imagelib.rawToJPEG(srcPath, destPath);
+		if (success){
+			sendResp("OK")
+		}else{
+			sendResp("Failed to convert RAW to JPEG");
+		}
+	} else {
+		console.log("Failed to load lib: imagelib");
+	}
+}