From f09beec27fef01e4569bab51e12bfd9968b0cd71 Mon Sep 17 00:00:00 2001 From: Martin Hebnes Pedersen Date: Wed, 3 Jan 2024 21:35:04 +0100 Subject: [PATCH] Align auto-convert feature between web gui and cli We have been auto-converting images for the web gui, while prompting the user from the CLI composer. Given that the CLI prompt is causing issues with the new "non-interactive" cli composer, I believe it's reasonable to avoid this complexity by doing auto-convertion on the command line aswell. We could consider adding a config options to let the user control this behavior in the future. But until then, let's keep it simple. Resolves #431 --- attachment.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++ cli_composer.go | 37 ++++-------------------- convert_image.go | 58 ------------------------------------- http.go | 26 +++-------------- 4 files changed, 85 insertions(+), 111 deletions(-) create mode 100644 attachment.go delete mode 100644 convert_image.go diff --git a/attachment.go b/attachment.go new file mode 100644 index 00000000..75e3e8be --- /dev/null +++ b/attachment.go @@ -0,0 +1,75 @@ +package main + +import ( + "bytes" + "image" + "image/jpeg" + "io" + "log" + "mime" + "path" + "path/filepath" + "strings" + + "github.com/la5nta/wl2k-go/fbb" + "github.com/nfnt/resize" +) + +func addAttachment(msg *fbb.Message, filename string, contentType string, r io.Reader) error { + p, err := io.ReadAll(r) + if err != nil { + return err + } + if ok, mediaType := isConvertableImageMediaType(filename, contentType); ok { + log.Printf("Auto converting '%s' [%s]...", filename, mediaType) + if converted, err := convertImage(p); err != nil { + log.Printf("Error converting image: %s", err) + } else { + log.Printf("Done converting '%s'.", filename) + ext := filepath.Ext(filename) + filename = filename[:len(filename)-len(ext)] + ".jpg" + p = converted + } + } + msg.AddFile(fbb.NewFile(filename, p)) + return nil +} + +func isConvertableImageMediaType(filename, contentType string) (convertable bool, mediaType string) { + if contentType != "" { + mediaType, _, _ = mime.ParseMediaType(contentType) + } + if mediaType == "" { + mediaType = mime.TypeByExtension(path.Ext(filename)) + } + + switch mediaType { + case "image/svg+xml": + // This is a text file + return false, mediaType + default: + return strings.HasPrefix(mediaType, "image/"), mediaType + } +} + +func convertImage(orig []byte) ([]byte, error) { + img, _, err := image.Decode(bytes.NewReader(orig)) + if err != nil { + return nil, err + } + + // Scale down + if img.Bounds().Dx() > 600 { + img = resize.Resize(600, 0, img, resize.NearestNeighbor) + } + + // Re-encode as low quality jpeg + var buf bytes.Buffer + if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 40}); err != nil { + return orig, err + } + if buf.Len() >= len(orig) { + return orig, nil + } + return buf.Bytes(), nil +} diff --git a/cli_composer.go b/cli_composer.go index d46e70be..030b5d20 100644 --- a/cli_composer.go +++ b/cli_composer.go @@ -157,13 +157,11 @@ func noninteractiveComposeMessage(from string, subject string, attachments []str msg.SetSubject(subject) // Handle Attachments. Since we're not interactive, treat errors as fatal so the user can fix - for _, filename := range attachments { - file, err := readAttachment(filename) - if err != nil { + for _, path := range attachments { + if err := addAttachmentFromPath(msg, path); err != nil { fmt.Fprint(os.Stderr, err.Error()+"\nAborting! (Message not posted)\n") os.Exit(1) } - msg.AddFile(file) } // Read the message body from stdin @@ -247,45 +245,22 @@ func interactiveComposeMessage(replyMsg *fbb.Message) { if path == "" { break } - - file, err := readAttachment(path) - if err != nil { + if err := addAttachmentFromPath(msg, path); err != nil { log.Println(err) continue } - - msg.AddFile(file) } fmt.Println(msg) postMessage(msg) } -func readAttachment(path string) (*fbb.File, error) { +func addAttachmentFromPath(msg *fbb.Message, path string) error { f, err := os.Open(path) if err != nil { - return nil, err + return err } defer f.Close() - - name := filepath.Base(path) - - var resizeImage bool - if isConvertableImageMediaType(name, "") { - fmt.Print("This seems to be an image. Auto resize? [Y/n]: ") - ans := readLine() - resizeImage = ans == "" || strings.EqualFold("y", ans) - } - - var data []byte - - data, err = ioutil.ReadAll(f) - if resizeImage { - data, err = convertImage(data) - ext := filepath.Ext(name) - name = name[:len(name)-len(ext)] + ".jpg" - } - - return fbb.NewFile(name, data), err + return addAttachment(msg, filepath.Base(path), "", f) } var stdin *bufio.Reader diff --git a/convert_image.go b/convert_image.go deleted file mode 100644 index c67329c9..00000000 --- a/convert_image.go +++ /dev/null @@ -1,58 +0,0 @@ -// Copyright 2016 Martin Hebnes Pedersen (LA5NTA). All rights reserved. -// Use of this source code is governed by the MIT-license that can be -// found in the LICENSE file. - -package main - -import ( - "bytes" - "image" - _ "image/gif" - "image/jpeg" - _ "image/png" - "mime" - "path" - "strings" - - "github.com/nfnt/resize" -) - -func isConvertableImageMediaType(filename, contentType string) bool { - var mediaType string - if contentType != "" { - mediaType, _, _ = mime.ParseMediaType(contentType) - } - if mediaType == "" { - mediaType = mime.TypeByExtension(path.Ext(filename)) - } - - switch mediaType { - case "image/svg+xml": - // This is a text file - return false - default: - return strings.HasPrefix(mediaType, "image/") - } -} - -func convertImage(orig []byte) ([]byte, error) { - img, _, err := image.Decode(bytes.NewReader(orig)) - if err != nil { - return nil, err - } - - // Scale down - if img.Bounds().Dx() > 600 { - img = resize.Resize(600, 0, img, resize.NearestNeighbor) - } - - // Re-encode as low quality jpeg - var buf bytes.Buffer - if err := jpeg.Encode(&buf, img, &jpeg.Options{Quality: 40}); err != nil { - return orig, err - } - if buf.Len() >= len(orig) { - return orig, nil - } - return buf.Bytes(), nil -} diff --git a/http.go b/http.go index 834eee61..591f5969 100644 --- a/http.go +++ b/http.go @@ -266,7 +266,7 @@ func postOutboundMessageHandler(w http.ResponseWriter, r *http.Request) { if r.MultipartForm != nil { files := r.MultipartForm.File["files"] for _, f := range files { - err := attachFile(f, msg) + err := addAttachmentFromMultipartFile(msg, f) switch err := err.(type) { case nil: // No problem @@ -345,7 +345,7 @@ func postOutboundMessageHandler(w http.ResponseWriter, r *http.Request) { _, _ = fmt.Fprintf(w, "Message posted (%.2f kB)", float64(buf.Len()/1024)) } -func attachFile(f *multipart.FileHeader, msg *fbb.Message) error { +func addAttachmentFromMultipartFile(msg *fbb.Message, f *multipart.FileHeader) error { // For some unknown reason, we receive this empty unnamed file when no // attachment is provided. Prior to Go 1.10, this was filtered by // multipart.Reader. @@ -361,28 +361,10 @@ func attachFile(f *multipart.FileHeader, msg *fbb.Message) error { if err != nil { return HTTPError{err, http.StatusInternalServerError} } - - p, err := io.ReadAll(file) - _ = file.Close() - if err != nil { + defer file.Close() + if err := addAttachment(msg, f.Filename, f.Header.Get("Content-Type"), file); err != nil { return HTTPError{err, http.StatusInternalServerError} } - - if isConvertableImageMediaType(f.Filename, f.Header.Get("Content-Type")) { - log.Printf("Auto converting '%s' [%s]...", f.Filename, f.Header.Get("Content-Type")) - - if converted, err := convertImage(p); err != nil { - log.Printf("Error converting image: %s", err) - } else { - log.Printf("Done converting '%s'.", f.Filename) - - ext := path.Ext(f.Filename) - f.Filename = f.Filename[:len(f.Filename)-len(ext)] + ".jpg" - p = converted - } - } - - msg.AddFile(fbb.NewFile(f.Filename, p)) return nil }