Skip to content
Open
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: 0 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,5 @@
# Executables
*.exe

# Internal Test files
*_test.go

# internal tools
/tools
143 changes: 134 additions & 9 deletions pkg/connector/handlers/image.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
package handlers

import (
"bytes"
"context"
"encoding/binary"
"encoding/json"
"fmt"
"image"
_ "image/gif"
_ "image/jpeg"
_ "image/png"
"net/http"
"strings"
"time"

_ "golang.org/x/image/webp"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"

"github.com/highesttt/matrix-line-messenger/pkg/line"
)
Expand Down Expand Up @@ -105,8 +114,22 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int
}

// Upload to Matrix
imageMedia := lineImageMediaInfo(imgData)
if imageMedia.usedMimeFallback {
h.Log.Debug().
Int("size_bytes", len(imgData)).
Msg("Falling back to JPEG MIME type for LINE image")
}
if imageMedia.decodeErr != nil {
h.Log.Debug().
Err(imageMedia.decodeErr).
Str("mime_type", imageMedia.mimeType).
Int("size_bytes", len(imgData)).
Msg("Could not decode LINE image dimensions")
}
Comment thread
Adri11334 marked this conversation as resolved.

uploadStart := time.Now()
mxc, file, err := intent.UploadMedia(ctx, portal.MXID, imgData, "image.jpg", "image/jpeg")
mxc, file, err := intent.UploadMedia(ctx, portal.MXID, imgData, imageMedia.fileName, imageMedia.mimeType)
uploadDuration := time.Since(uploadStart)
if err != nil {
h.Log.Error().
Expand Down Expand Up @@ -135,19 +158,121 @@ func (h *Handler) ConvertImage(ctx context.Context, portal *bridgev2.Portal, int
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{
{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgImage,
Body: "image.jpg",
URL: mxc,
File: file,
RelatesTo: relatesTo,
},
Type: event.EventMessage,
Content: lineImageEventContent(mxc, file, imageMedia.fileName, imageMedia.info, relatesTo),
},
},
}, nil
}

func lineImageEventContent(mxc id.ContentURIString, file *event.EncryptedFileInfo, fileName string, info *event.FileInfo, relatesTo *event.RelatesTo) *event.MessageEventContent {
return &event.MessageEventContent{
MsgType: event.MsgImage,
Body: fileName,
URL: mxc,
File: file,
Info: info,
RelatesTo: relatesTo,
}
}

type lineImageMedia struct {
fileName string
mimeType string
info *event.FileInfo
usedMimeFallback bool
decodeErr error
}

func lineImageMediaInfo(data []byte) lineImageMedia {
mimeType, usedFallback := detectLineImageMimeType(data)
info := &event.FileInfo{
MimeType: mimeType,
Size: len(data),
}

var decodeErr error
if config, _, err := image.DecodeConfig(bytes.NewReader(data)); err == nil {
info.Width = config.Width
info.Height = config.Height
} else if !usedFallback && shouldLogLineImageDecodeError(mimeType) {
decodeErr = err
}

return lineImageMedia{
fileName: "image." + imageExtensionForMIME(mimeType),
mimeType: mimeType,
info: info,
usedMimeFallback: usedFallback,
decodeErr: decodeErr,
}
}

func detectLineImageMimeType(data []byte) (string, bool) {
mimeType := normalizedImageMIMEType(http.DetectContentType(data))
if strings.HasPrefix(mimeType, "image/") {
return mimeType, false
}
Comment thread
Adri11334 marked this conversation as resolved.

if len(data) >= 12 && lineImageBrandAt(data, 4) == [4]byte{'f', 't', 'y', 'p'} {
boxSize := binary.BigEndian.Uint32(data[:4])
if boxSize >= 16 && uint64(boxSize) <= uint64(len(data)) {
detected := ""
for offset, end := 8, int(boxSize); offset+4 <= end; {
switch lineImageBrandAt(data, offset) {
case [4]byte{'a', 'v', 'i', 'f'}, [4]byte{'a', 'v', 'i', 's'}:
return "image/avif", false
case [4]byte{'h', 'e', 'i', 'c'}, [4]byte{'h', 'e', 'i', 'x'}, [4]byte{'h', 'e', 'v', 'c'}, [4]byte{'h', 'e', 'v', 'x'}:
return "image/heic", false
case [4]byte{'h', 'e', 'i', 'f'}, [4]byte{'h', 'e', 'i', 'm'}, [4]byte{'h', 'e', 'i', 's'}, [4]byte{'m', 'i', 'f', '1'}, [4]byte{'m', 's', 'f', '1'}:
detected = "image/heif"
}
if offset == 8 {
offset = 16
} else {
offset += 4
}
}
if detected != "" {
return detected, false
}
}
}
Comment thread
Adri11334 marked this conversation as resolved.

return "image/jpeg", true
}

func lineImageBrandAt(data []byte, offset int) [4]byte {
return [4]byte{data[offset], data[offset+1], data[offset+2], data[offset+3]}
}

func imageExtensionForMIME(mimeType string) string {
switch normalizedMIMEType := normalizedImageMIMEType(mimeType); normalizedMIMEType {
case "image/jpeg":
return "jpg"
case "image/svg+xml":
return "svg"
case "image/x-icon", "image/vnd.microsoft.icon":
return "ico"
case "image/png", "image/gif", "image/webp", "image/heic", "image/heif", "image/avif", "image/bmp", "image/tiff":
return strings.TrimPrefix(normalizedMIMEType, "image/")
}
return "jpg"
}

func shouldLogLineImageDecodeError(mimeType string) bool {
switch normalizedImageMIMEType(mimeType) {
case "image/jpeg", "image/png", "image/gif", "image/webp":
return true
}
return false
}

func normalizedImageMIMEType(mimeType string) string {
normalizedMIMEType, _, _ := strings.Cut(mimeType, ";")
return strings.ToLower(strings.TrimSpace(normalizedMIMEType))
}
Comment thread
Adri11334 marked this conversation as resolved.
Comment thread
Adri11334 marked this conversation as resolved.

func lineMediaCategory(metadata map[string]string) string {
if metadata == nil || metadata["MEDIA_CONTENT_INFO"] == "" {
return ""
Expand Down
Loading