fix: include image metadata for sends
This commit is contained in:
parent
3031a34ff2
commit
b4ca2e35b0
@ -56,6 +56,7 @@
|
||||
- Send: strip a leading `+` from phone-number recipients before building WhatsApp JIDs. (#74 — thanks @FrederickStempfle)
|
||||
- Search: keep FTS5 enabled after reopening existing databases with already-applied migrations. (#185 — thanks @iamhitarth)
|
||||
- Send: add `send text --reply-to` for quoted replies, with sender inference for synced group messages. (#154 — thanks @draix)
|
||||
- Send: validate image uploads and include image dimensions plus a JPEG thumbnail for better client rendering.
|
||||
- Send: keep the connection alive briefly after successful sends so retry receipts can repair first-send session gaps. (#89 — thanks @alexph-dev)
|
||||
- Send: bound send attempts and reconnect once for stale-session/time-out failures instead of hanging indefinitely. (#115 — thanks @0xatrilla)
|
||||
- Send: include the Opus codec parameter when sending OGG audio so WhatsApp delivers it as audio. (#41 — thanks @emre6943)
|
||||
|
||||
@ -1,9 +1,16 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/draw"
|
||||
_ "image/gif"
|
||||
"image/jpeg"
|
||||
_ "image/png"
|
||||
"math"
|
||||
"mime"
|
||||
"net/http"
|
||||
@ -24,6 +31,7 @@ import (
|
||||
)
|
||||
|
||||
const maxSendFileSize = 100 * 1024 * 1024
|
||||
const imageThumbnailMaxDimension = 96
|
||||
const voiceWaveformSamples = 64
|
||||
const voiceWaveformMax = 100
|
||||
|
||||
@ -91,16 +99,11 @@ func sendFile(ctx context.Context, a interface {
|
||||
|
||||
switch mediaType {
|
||||
case "image":
|
||||
msg.ImageMessage = &waProto.ImageMessage{
|
||||
URL: proto.String(up.URL),
|
||||
DirectPath: proto.String(up.DirectPath),
|
||||
MediaKey: up.MediaKey,
|
||||
FileEncSHA256: up.FileEncSHA256,
|
||||
FileSHA256: up.FileSHA256,
|
||||
FileLength: proto.Uint64(up.FileLength),
|
||||
Mimetype: proto.String(mimeType),
|
||||
Caption: proto.String(opts.caption),
|
||||
imageMsg, err := newImageMessage(up, mimeType, opts.caption, data)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
msg.ImageMessage = imageMsg
|
||||
case "video":
|
||||
msg.VideoMessage = &waProto.VideoMessage{
|
||||
URL: proto.String(up.URL),
|
||||
@ -166,6 +169,83 @@ func sendFile(ctx context.Context, a interface {
|
||||
}, nil
|
||||
}
|
||||
|
||||
func newImageMessage(up whatsmeow.UploadResponse, mimeType, caption string, data []byte) (*waProto.ImageMessage, error) {
|
||||
cfg, _, err := image.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid image data: %w", err)
|
||||
}
|
||||
if cfg.Width <= 0 || cfg.Height <= 0 {
|
||||
return nil, fmt.Errorf("invalid image dimensions: %dx%d", cfg.Width, cfg.Height)
|
||||
}
|
||||
|
||||
msg := &waProto.ImageMessage{
|
||||
URL: proto.String(up.URL),
|
||||
DirectPath: proto.String(up.DirectPath),
|
||||
MediaKey: up.MediaKey,
|
||||
FileEncSHA256: up.FileEncSHA256,
|
||||
FileSHA256: up.FileSHA256,
|
||||
FileLength: proto.Uint64(up.FileLength),
|
||||
Mimetype: proto.String(mimeType),
|
||||
Caption: proto.String(caption),
|
||||
Height: proto.Uint32(uint32(cfg.Height)),
|
||||
Width: proto.Uint32(uint32(cfg.Width)),
|
||||
}
|
||||
if thumbnail, err := imageJPEGThumbnail(data); err == nil && len(thumbnail) > 0 {
|
||||
msg.JPEGThumbnail = thumbnail
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func imageJPEGThumbnail(data []byte) ([]byte, error) {
|
||||
src, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bounds := src.Bounds()
|
||||
srcW, srcH := bounds.Dx(), bounds.Dy()
|
||||
if srcW <= 0 || srcH <= 0 {
|
||||
return nil, fmt.Errorf("invalid image dimensions: %dx%d", srcW, srcH)
|
||||
}
|
||||
|
||||
dstW, dstH := scaledDimensions(srcW, srcH, imageThumbnailMaxDimension)
|
||||
dst := image.NewRGBA(image.Rect(0, 0, dstW, dstH))
|
||||
draw.Draw(dst, dst.Bounds(), &image.Uniform{C: color.White}, image.Point{}, draw.Src)
|
||||
for y := 0; y < dstH; y++ {
|
||||
for x := 0; x < dstW; x++ {
|
||||
srcX := bounds.Min.X + x*srcW/dstW
|
||||
srcY := bounds.Min.Y + y*srcH/dstH
|
||||
dst.Set(x, y, src.At(srcX, srcY))
|
||||
}
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
if err := jpeg.Encode(&out, dst, &jpeg.Options{Quality: 75}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func scaledDimensions(width, height, maxDimension int) (int, int) {
|
||||
if width <= 0 || height <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
if maxDimension <= 0 || (width <= maxDimension && height <= maxDimension) {
|
||||
return width, height
|
||||
}
|
||||
if width >= height {
|
||||
scaledHeight := height * maxDimension / width
|
||||
if scaledHeight < 1 {
|
||||
scaledHeight = 1
|
||||
}
|
||||
return maxDimension, scaledHeight
|
||||
}
|
||||
scaledWidth := width * maxDimension / height
|
||||
if scaledWidth < 1 {
|
||||
scaledWidth = 1
|
||||
}
|
||||
return scaledWidth, maxDimension
|
||||
}
|
||||
|
||||
func newAudioMessage(up whatsmeow.UploadResponse, mimeType string, ptt bool, meta voiceNoteMetadata) *waProto.AudioMessage {
|
||||
msg := &waProto.AudioMessage{
|
||||
URL: proto.String(up.URL),
|
||||
|
||||
@ -1,8 +1,13 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@ -110,6 +115,68 @@ func TestNewAudioMessageAttachesPTTMetadata(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewImageMessageAttachesDimensionsAndThumbnail(t *testing.T) {
|
||||
img := image.NewRGBA(image.Rect(0, 0, 120, 60))
|
||||
for y := 0; y < 60; y++ {
|
||||
for x := 0; x < 120; x++ {
|
||||
img.Set(x, y, color.RGBA{R: uint8(x), G: uint8(y), B: 120, A: 255})
|
||||
}
|
||||
}
|
||||
var data bytes.Buffer
|
||||
if err := png.Encode(&data, img); err != nil {
|
||||
t.Fatalf("png.Encode: %v", err)
|
||||
}
|
||||
|
||||
up := whatsmeow.UploadResponse{
|
||||
URL: "https://upload",
|
||||
DirectPath: "/path",
|
||||
MediaKey: []byte("key"),
|
||||
FileEncSHA256: []byte("enc"),
|
||||
FileSHA256: []byte("plain"),
|
||||
FileLength: uint64(data.Len()),
|
||||
}
|
||||
msg, err := newImageMessage(up, "image/png", "caption", data.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("newImageMessage: %v", err)
|
||||
}
|
||||
if msg.GetWidth() != 120 || msg.GetHeight() != 60 {
|
||||
t.Fatalf("dimensions = %dx%d, want 120x60", msg.GetWidth(), msg.GetHeight())
|
||||
}
|
||||
if msg.GetCaption() != "caption" {
|
||||
t.Fatalf("caption = %q", msg.GetCaption())
|
||||
}
|
||||
if len(msg.GetJPEGThumbnail()) == 0 {
|
||||
t.Fatalf("missing JPEG thumbnail")
|
||||
}
|
||||
if _, err := jpeg.Decode(bytes.NewReader(msg.GetJPEGThumbnail())); err != nil {
|
||||
t.Fatalf("thumbnail is not JPEG: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewImageMessageRejectsInvalidImageData(t *testing.T) {
|
||||
_, err := newImageMessage(whatsmeow.UploadResponse{}, "image/png", "", []byte("not an image"))
|
||||
if err == nil || !strings.Contains(err.Error(), "invalid image data") {
|
||||
t.Fatalf("expected invalid image error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaledDimensions(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
width, height int
|
||||
wantW, wantH int
|
||||
}{
|
||||
{width: 120, height: 60, wantW: 96, wantH: 48},
|
||||
{width: 60, height: 120, wantW: 48, wantH: 96},
|
||||
{width: 40, height: 30, wantW: 40, wantH: 30},
|
||||
{width: 1, height: 1000, wantW: 1, wantH: 96},
|
||||
} {
|
||||
gotW, gotH := scaledDimensions(tc.width, tc.height, imageThumbnailMaxDimension)
|
||||
if gotW != tc.wantW || gotH != tc.wantH {
|
||||
t.Fatalf("scaledDimensions(%d,%d) = %dx%d, want %dx%d", tc.width, tc.height, gotW, gotH, tc.wantW, tc.wantH)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaveformFromPCM16LE(t *testing.T) {
|
||||
data := make([]byte, voiceWaveformSamples*4)
|
||||
for i := 0; i < voiceWaveformSamples*2; i++ {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user