Fix preview for icon attachment
It will fall back to original image and return preview accordingly.
@ -21,6 +21,7 @@ import (
|
||||
"github.com/disintegration/imaging"
|
||||
"github.com/edwvee/exiffix"
|
||||
"github.com/gabriel-vasile/mimetype"
|
||||
"github.com/mat/besticon/ico"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -588,11 +589,16 @@ func (svc attachment) extractMimetypeS(file io.ReadSeeker) (mType string, err er
|
||||
}
|
||||
|
||||
func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment) (err error) {
|
||||
if !strings.HasPrefix(att.Meta.Original.Mimetype, "image/") || att.Meta.Original.Mimetype == "image/x-icon" {
|
||||
if !strings.HasPrefix(att.Meta.Original.Mimetype, "image/") {
|
||||
// Only supporting previews from images (for now)
|
||||
return
|
||||
}
|
||||
|
||||
const (
|
||||
iconMimetype = "image/x-icon"
|
||||
iconExtension = "ico"
|
||||
)
|
||||
|
||||
var (
|
||||
preview image.Image
|
||||
opts []imaging.EncodeOption
|
||||
@ -603,48 +609,55 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
|
||||
imaging.JPEG: "image/jpeg",
|
||||
imaging.GIF: "image/gif",
|
||||
}
|
||||
|
||||
f2e = map[imaging.Format]string{
|
||||
imaging.JPEG: "jpg",
|
||||
imaging.GIF: "gif",
|
||||
}
|
||||
isIcon = att.Meta.Original.Mimetype == iconMimetype
|
||||
)
|
||||
|
||||
if _, err = original.Seek(0, 0); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
|
||||
return errors.Internal("could not get format from extension '%s'", att.Meta.Original.Extension).Wrap(err)
|
||||
}
|
||||
|
||||
previewFormat = format
|
||||
|
||||
if imaging.JPEG == format {
|
||||
// Rotate image if needed
|
||||
// if preview, _, err = exiffix.Decode(original); err != nil {
|
||||
// return fmt.Errorf("Could not decode EXIF from JPEG", err)
|
||||
// }
|
||||
preview, _, _ = exiffix.Decode(original)
|
||||
}
|
||||
|
||||
if imaging.GIF == format {
|
||||
// Decode all and check loops & delay to determine if GIF is animated or not
|
||||
if cfg, err := gif.DecodeAll(original); err == nil {
|
||||
animated = cfg.LoopCount > 0 || len(cfg.Delay) > 1
|
||||
|
||||
// Use first image for the preview
|
||||
preview = cfg.Image[0]
|
||||
} else {
|
||||
return errors.Internal("Could not decode gif config").Wrap(err)
|
||||
if isIcon {
|
||||
preview, err = ico.Decode(original)
|
||||
if err != nil {
|
||||
return errors.Internal("Could not decode ico config").Wrap(err)
|
||||
}
|
||||
} else {
|
||||
if format, err = imaging.FormatFromExtension(att.Meta.Original.Extension); err != nil {
|
||||
return errors.Internal("could not get format from extension '%s'", att.Meta.Original.Extension).Wrap(err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Use GIF preview for GIFs and JPEG for everything else!
|
||||
previewFormat = imaging.JPEG
|
||||
previewFormat = format
|
||||
|
||||
// Store with a bit lower quality
|
||||
opts = append(opts, imaging.JPEGQuality(85))
|
||||
if imaging.JPEG == format {
|
||||
// Rotate image if needed
|
||||
// if preview, _, err = exiffix.Decode(original); err != nil {
|
||||
// return fmt.Errorf("Could not decode EXIF from JPEG", err)
|
||||
// }
|
||||
preview, _, _ = exiffix.Decode(original)
|
||||
}
|
||||
|
||||
if imaging.GIF == format {
|
||||
// Decode all and check loops & delay to determine if GIF is animated or not
|
||||
if cfg, err := gif.DecodeAll(original); err == nil {
|
||||
animated = cfg.LoopCount > 0 || len(cfg.Delay) > 1
|
||||
|
||||
// Use first image for the preview
|
||||
preview = cfg.Image[0]
|
||||
} else {
|
||||
return errors.Internal("Could not decode gif config").Wrap(err)
|
||||
}
|
||||
|
||||
} else {
|
||||
// Use GIF preview for GIFs and JPEG for everything else!
|
||||
previewFormat = imaging.JPEG
|
||||
|
||||
// Store with a bit lower quality
|
||||
opts = append(opts, imaging.JPEGQuality(85))
|
||||
}
|
||||
}
|
||||
|
||||
// In case of JPEG we decode the image and rotate it beforehand
|
||||
@ -676,8 +689,14 @@ func (svc attachment) processImage(original io.ReadSeeker, att *types.Attachment
|
||||
|
||||
meta := att.SetPreviewImageMeta(width, height, false)
|
||||
meta.Size = int64(buf.Len())
|
||||
meta.Mimetype = f2m[previewFormat]
|
||||
meta.Extension = f2e[previewFormat]
|
||||
|
||||
if isIcon {
|
||||
meta.Mimetype = iconMimetype
|
||||
meta.Extension = iconExtension
|
||||
} else {
|
||||
meta.Mimetype = f2m[previewFormat]
|
||||
meta.Extension = f2e[previewFormat]
|
||||
}
|
||||
|
||||
// Can and how we make a preview of this attachment?
|
||||
att.PreviewUrl = svc.objects.Preview(att.ID, meta.Extension)
|
||||
|
||||
1
server/go.mod
vendored
@ -50,6 +50,7 @@ require (
|
||||
github.com/lestrrat-go/strftime v1.0.6
|
||||
github.com/lib/pq v1.10.5
|
||||
github.com/markbates/goth v1.71.1
|
||||
github.com/mat/besticon v3.12.0+incompatible
|
||||
github.com/mattn/go-sqlite3 v1.14.12
|
||||
github.com/microcosm-cc/bluemonday v1.0.18
|
||||
github.com/minio/minio-go/v6 v6.0.57
|
||||
|
||||
2
server/go.sum
vendored
@ -423,6 +423,8 @@ github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/markbates/going v1.0.0/go.mod h1:I6mnB4BPnEeqo85ynXIx1ZFLLbtiLHNXVgWeFO9OGOA=
|
||||
github.com/markbates/goth v1.71.1 h1:MYXrNZwtMsCqVouRv1nhz8APZaMGOsymTHupVvzJBUM=
|
||||
github.com/markbates/goth v1.71.1/go.mod h1:X6xdNgpapSENS0O35iTBBcMHoJDQDfI9bJl+APCkYMc=
|
||||
github.com/mat/besticon v3.12.0+incompatible h1:1KTD6wisfjfnX+fk9Kx/6VEZL+MAW1LhCkL9Q47H9Bg=
|
||||
github.com/mat/besticon v3.12.0+incompatible/go.mod h1:mA1auQYHt6CW5e7L9HJLmqVQC8SzNk2gVwouO0AbiEU=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0 h1:RXbVD2UAl7A7nOTR4u7E3ILa4IbtvKBHw64LDsmu9hU=
|
||||
github.com/mattermost/xml-roundtrip-validator v0.1.0/go.mod h1:qccnGMcpgwcNaBnxqpJpWWUiPNr5H3O8eDgGV9gT5To=
|
||||
github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||
|
||||
21
server/vendor/github.com/mat/besticon/LICENSE
generated
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015-2020 Matthias Lüdtke, Hamburg - https://github.com/mat
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
5114
server/vendor/github.com/mat/besticon/NOTICES
generated
vendored
Normal file
BIN
server/vendor/github.com/mat/besticon/ico/addthis.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
server/vendor/github.com/mat/besticon/ico/besticon.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
server/vendor/github.com/mat/besticon/ico/broken.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 32 B |
BIN
server/vendor/github.com/mat/besticon/ico/codeplex.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
server/vendor/github.com/mat/besticon/ico/favicon.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
server/vendor/github.com/mat/besticon/ico/github.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 6.4 KiB |
261
server/vendor/github.com/mat/besticon/ico/ico.go
generated
vendored
Normal file
@ -0,0 +1,261 @@
|
||||
// Package ico registers image.Decode and DecodeConfig support
|
||||
// for the icon (container) format.
|
||||
package ico
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"image"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
|
||||
"image/png"
|
||||
|
||||
"golang.org/x/image/bmp"
|
||||
)
|
||||
|
||||
type icondir struct {
|
||||
Reserved uint16
|
||||
Type uint16
|
||||
Count uint16
|
||||
Entries []icondirEntry
|
||||
}
|
||||
|
||||
type icondirEntry struct {
|
||||
Width byte
|
||||
Height byte
|
||||
PaletteCount byte
|
||||
Reserved byte
|
||||
ColorPlanes uint16
|
||||
BitsPerPixel uint16
|
||||
Size uint32
|
||||
Offset uint32
|
||||
}
|
||||
|
||||
func (dir *icondir) FindBestIcon() *icondirEntry {
|
||||
if len(dir.Entries) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
best := dir.Entries[0]
|
||||
for _, e := range dir.Entries {
|
||||
if (e.width() > best.width()) && (e.height() > best.height()) {
|
||||
best = e
|
||||
}
|
||||
}
|
||||
return &best
|
||||
}
|
||||
|
||||
// ParseIco parses the icon and returns meta information for the icons as icondir.
|
||||
func ParseIco(r io.Reader) (*icondir, error) {
|
||||
dir := icondir{}
|
||||
|
||||
var err error
|
||||
err = binary.Read(r, binary.LittleEndian, &dir.Reserved)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &dir.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &dir.Count)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i := uint16(0); i < dir.Count; i++ {
|
||||
entry := icondirEntry{}
|
||||
e := parseIcondirEntry(r, &entry)
|
||||
if e != nil {
|
||||
return nil, e
|
||||
}
|
||||
dir.Entries = append(dir.Entries, entry)
|
||||
}
|
||||
|
||||
return &dir, err
|
||||
}
|
||||
|
||||
func parseIcondirEntry(r io.Reader, e *icondirEntry) error {
|
||||
err := binary.Read(r, binary.LittleEndian, e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type dibHeader struct {
|
||||
dibHeaderSize uint32
|
||||
width uint32
|
||||
height uint32
|
||||
}
|
||||
|
||||
func (e *icondirEntry) ColorCount() int {
|
||||
if e.PaletteCount == 0 {
|
||||
return 256
|
||||
}
|
||||
return int(e.PaletteCount)
|
||||
}
|
||||
|
||||
func (e *icondirEntry) width() int {
|
||||
if e.Width == 0 {
|
||||
return 256
|
||||
}
|
||||
return int(e.Width)
|
||||
}
|
||||
|
||||
func (e *icondirEntry) height() int {
|
||||
if e.Height == 0 {
|
||||
return 256
|
||||
}
|
||||
return int(e.Height)
|
||||
}
|
||||
|
||||
// DecodeConfig returns just the dimensions of the largest image
|
||||
// contained in the icon withou decoding the entire icon file.
|
||||
func DecodeConfig(r io.Reader) (image.Config, error) {
|
||||
dir, err := ParseIco(r)
|
||||
if err != nil {
|
||||
return image.Config{}, err
|
||||
}
|
||||
|
||||
best := dir.FindBestIcon()
|
||||
if best == nil {
|
||||
return image.Config{}, errInvalid
|
||||
}
|
||||
return image.Config{Width: best.width(), Height: best.height()}, nil
|
||||
}
|
||||
|
||||
// The bitmap header structure we read from an icondirEntry
|
||||
type bitmapHeaderRead struct {
|
||||
Size uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
Planes uint16
|
||||
BitCount uint16
|
||||
Compression uint32
|
||||
ImageSize uint32
|
||||
XPixelsPerMeter uint32
|
||||
YPixelsPerMeter uint32
|
||||
ColorsUsed uint32
|
||||
ColorsImportant uint32
|
||||
}
|
||||
|
||||
// The bitmap header structure we need to generate for bmp.Decode()
|
||||
type bitmapHeaderWrite struct {
|
||||
sigBM [2]byte
|
||||
fileSize uint32
|
||||
resverved [2]uint16
|
||||
pixOffset uint32
|
||||
Size uint32
|
||||
Width uint32
|
||||
Height uint32
|
||||
Planes uint16
|
||||
BitCount uint16
|
||||
Compression uint32
|
||||
ImageSize uint32
|
||||
XPixelsPerMeter uint32
|
||||
YPixelsPerMeter uint32
|
||||
ColorsUsed uint32
|
||||
ColorsImportant uint32
|
||||
}
|
||||
|
||||
var errInvalid = errors.New("ico: invalid ICO image")
|
||||
|
||||
// Decode returns the largest image contained in the icon
|
||||
// which might be a bmp or png
|
||||
func Decode(r io.Reader) (image.Image, error) {
|
||||
icoBytes, err := ioutil.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
r = bytes.NewReader(icoBytes)
|
||||
dir, err := ParseIco(r)
|
||||
if err != nil {
|
||||
return nil, errInvalid
|
||||
}
|
||||
|
||||
best := dir.FindBestIcon()
|
||||
if best == nil {
|
||||
return nil, errInvalid
|
||||
}
|
||||
|
||||
return parseImage(best, icoBytes)
|
||||
}
|
||||
|
||||
func parseImage(entry *icondirEntry, icoBytes []byte) (image.Image, error) {
|
||||
r := bytes.NewReader(icoBytes)
|
||||
r.Seek(int64(entry.Offset), 0)
|
||||
|
||||
// Try PNG first then BMP
|
||||
img, err := png.Decode(r)
|
||||
if err != nil {
|
||||
return parseBMP(entry, icoBytes)
|
||||
}
|
||||
return img, nil
|
||||
}
|
||||
|
||||
func parseBMP(entry *icondirEntry, icoBytes []byte) (image.Image, error) {
|
||||
bmpBytes, err := makeFullBMPBytes(entry, icoBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return bmp.Decode(bmpBytes)
|
||||
}
|
||||
|
||||
func makeFullBMPBytes(entry *icondirEntry, icoBytes []byte) (*bytes.Buffer, error) {
|
||||
r := bytes.NewReader(icoBytes)
|
||||
r.Seek(int64(entry.Offset), 0)
|
||||
|
||||
var err error
|
||||
h := bitmapHeaderRead{}
|
||||
|
||||
err = binary.Read(r, binary.LittleEndian, &h)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if h.Size != 40 || h.Planes != 1 {
|
||||
return nil, errInvalid
|
||||
}
|
||||
|
||||
var pixOffset uint32
|
||||
if h.ColorsUsed == 0 && h.BitCount <= 8 {
|
||||
pixOffset = 14 + 40 + 4*(1<<h.BitCount)
|
||||
} else {
|
||||
pixOffset = 14 + 40 + 4*h.ColorsUsed
|
||||
}
|
||||
|
||||
writeHeader := &bitmapHeaderWrite{
|
||||
sigBM: [2]byte{'B', 'M'},
|
||||
fileSize: 14 + 40 + uint32(len(icoBytes)), // correct? important?
|
||||
pixOffset: pixOffset,
|
||||
Size: 40,
|
||||
Width: uint32(h.Width),
|
||||
Height: uint32(h.Height / 2),
|
||||
Planes: h.Planes,
|
||||
BitCount: h.BitCount,
|
||||
Compression: h.Compression,
|
||||
ColorsUsed: h.ColorsUsed,
|
||||
ColorsImportant: h.ColorsImportant,
|
||||
}
|
||||
|
||||
buf := new(bytes.Buffer)
|
||||
if err = binary.Write(buf, binary.LittleEndian, writeHeader); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
io.CopyN(buf, r, int64(entry.Size))
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
const icoHeader = "\x00\x00\x01\x00"
|
||||
|
||||
func init() {
|
||||
image.RegisterFormat("ico", icoHeader, Decode, DecodeConfig)
|
||||
}
|
||||
BIN
server/vendor/github.com/mat/besticon/ico/wowhead.ico
generated
vendored
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
3
server/vendor/modules.txt
vendored
@ -333,6 +333,9 @@ github.com/markbates/goth/providers/github
|
||||
github.com/markbates/goth/providers/google
|
||||
github.com/markbates/goth/providers/linkedin
|
||||
github.com/markbates/goth/providers/openidConnect
|
||||
# github.com/mat/besticon v3.12.0+incompatible
|
||||
## explicit
|
||||
github.com/mat/besticon/ico
|
||||
# github.com/mattermost/xml-roundtrip-validator v0.1.0
|
||||
## explicit; go 1.14
|
||||
github.com/mattermost/xml-roundtrip-validator
|
||||
|
||||