thumbnailer_nfnt.go 7.6 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
// Copyright 2017 Vector Creations Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// +build !bimg

package thumbnailer

import (
	"image"
	"image/draw"
	// Imported for gif codec
	_ "image/gif"
	"image/jpeg"
	// Imported for png codec
	_ "image/png"
	"os"
	"time"

	log "github.com/Sirupsen/logrus"
31
	"github.com/matrix-org/dendrite/common/config"
32 33 34 35 36 37
	"github.com/matrix-org/dendrite/mediaapi/storage"
	"github.com/matrix-org/dendrite/mediaapi/types"
	"github.com/nfnt/resize"
)

// GenerateThumbnails generates the configured thumbnail sizes for the source file
38
func GenerateThumbnails(src types.Path, configs []config.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) {
39 40 41 42 43 44 45
	img, err := readFile(string(src))
	if err != nil {
		logger.WithError(err).WithField("src", src).Error("Failed to read src file")
		return false, err
	}
	for _, config := range configs {
		// Note: createThumbnail does locking based on activeThumbnailGeneration
46
		busy, err = createThumbnail(src, img, types.ThumbnailSize(config), mediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, logger)
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85
		if err != nil {
			logger.WithError(err).WithField("src", src).Error("Failed to generate thumbnails")
			return false, err
		}
		if busy {
			return true, nil
		}
	}
	return false, nil
}

// GenerateThumbnail generates the configured thumbnail size for the source file
func GenerateThumbnail(src types.Path, config types.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) {
	img, err := readFile(string(src))
	if err != nil {
		logger.WithError(err).WithFields(log.Fields{
			"src": src,
		}).Error("Failed to read src file")
		return false, err
	}
	// Note: createThumbnail does locking based on activeThumbnailGeneration
	busy, err = createThumbnail(src, img, config, mediaMetadata, activeThumbnailGeneration, maxThumbnailGenerators, db, logger)
	if err != nil {
		logger.WithError(err).WithFields(log.Fields{
			"src": src,
		}).Error("Failed to generate thumbnails")
		return false, err
	}
	if busy {
		return true, nil
	}
	return false, nil
}

func readFile(src string) (image.Image, error) {
	file, err := os.Open(src)
	if err != nil {
		return nil, err
	}
E
Erik Johnston 已提交
86
	defer file.Close() // nolint: errcheck
87 88 89 90 91 92 93 94 95

	img, _, err := image.Decode(file)
	if err != nil {
		return nil, err
	}

	return img, nil
}

E
Erik Johnston 已提交
96
func writeFile(img image.Image, dst string) (err error) {
97 98 99 100
	out, err := os.Create(dst)
	if err != nil {
		return err
	}
E
Erik Johnston 已提交
101
	defer (func() { err = out.Close() })()
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116

	return jpeg.Encode(out, img, &jpeg.Options{
		Quality: 85,
	})
}

// createThumbnail checks if the thumbnail exists, and if not, generates it
// Thumbnail generation is only done once for each non-existing thumbnail.
func createThumbnail(src types.Path, img image.Image, config types.ThumbnailSize, mediaMetadata *types.MediaMetadata, activeThumbnailGeneration *types.ActiveThumbnailGeneration, maxThumbnailGenerators int, db *storage.Database, logger *log.Entry) (busy bool, errorReturn error) {
	logger = logger.WithFields(log.Fields{
		"Width":        config.Width,
		"Height":       config.Height,
		"ResizeMethod": config.ResizeMethod,
	})

117 118 119 120 121
	// Check if request is larger than original
	if config.Width >= img.Bounds().Dx() && config.Height >= img.Bounds().Dy() {
		return false, nil
	}

122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145
	dst := GetThumbnailPath(src, config)

	// Note: getActiveThumbnailGeneration uses mutexes and conditions from activeThumbnailGeneration
	isActive, busy, err := getActiveThumbnailGeneration(dst, config, activeThumbnailGeneration, maxThumbnailGenerators, logger)
	if err != nil {
		return false, err
	}
	if busy {
		return true, nil
	}

	if isActive {
		// Note: This is an active request that MUST broadcastGeneration to wake up waiting goroutines!
		// Note: broadcastGeneration uses mutexes and conditions from activeThumbnailGeneration
		defer func() {
			// Note: errorReturn is the named return variable so we wrap this in a closure to re-evaluate the arguments at defer-time
			// if err := recover(); err != nil {
			// 	broadcastGeneration(dst, activeThumbnailGeneration, config, err.(error), logger)
			// 	panic(err)
			// }
			broadcastGeneration(dst, activeThumbnailGeneration, config, errorReturn, logger)
		}()
	}

146 147
	exists, err := isThumbnailExists(dst, config, mediaMetadata, db, logger)
	if err != nil || exists {
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
		return false, err
	}

	start := time.Now()
	width, height, err := adjustSize(dst, img, config.Width, config.Height, config.ResizeMethod == "crop", logger)
	if err != nil {
		return false, err
	}
	logger.WithFields(log.Fields{
		"ActualWidth":  width,
		"ActualHeight": height,
		"processTime":  time.Now().Sub(start),
	}).Info("Generated thumbnail")

	stat, err := os.Stat(string(dst))
	if err != nil {
		return false, err
	}

167
	thumbnailMetadata := &types.ThumbnailMetadata{
168 169 170 171 172 173 174 175
		MediaMetadata: &types.MediaMetadata{
			MediaID: mediaMetadata.MediaID,
			Origin:  mediaMetadata.Origin,
			// Note: the code currently always creates a JPEG thumbnail
			ContentType:   types.ContentType("image/jpeg"),
			FileSizeBytes: types.FileSizeBytes(stat.Size()),
		},
		ThumbnailSize: types.ThumbnailSize{
176 177
			Width:        config.Width,
			Height:       config.Height,
178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237
			ResizeMethod: config.ResizeMethod,
		},
	}

	err = db.StoreThumbnail(thumbnailMetadata)
	if err != nil {
		logger.WithError(err).WithFields(log.Fields{
			"ActualWidth":  width,
			"ActualHeight": height,
		}).Error("Failed to store thumbnail metadata in database.")
		return false, err
	}

	return false, nil
}

// adjustSize scales an image to fit within the provided width and height
// If the source aspect ratio is different to the target dimensions, one edge will be smaller than requested
// If crop is set to true, the image will be scaled to fill the width and height with any excess being cropped off
func adjustSize(dst types.Path, img image.Image, w, h int, crop bool, logger *log.Entry) (int, int, error) {
	var out image.Image
	var err error
	if crop {
		inAR := float64(img.Bounds().Dx()) / float64(img.Bounds().Dy())
		outAR := float64(w) / float64(h)

		var scaleW, scaleH uint
		if inAR > outAR {
			// input has shorter AR than requested output so use requested height and calculate width to match input AR
			scaleW = uint(float64(h) * inAR)
			scaleH = uint(h)
		} else {
			// input has taller AR than requested output so use requested width and calculate height to match input AR
			scaleW = uint(w)
			scaleH = uint(float64(w) / inAR)
		}

		scaled := resize.Resize(scaleW, scaleH, img, resize.Lanczos3)

		xoff := (scaled.Bounds().Dx() - w) / 2
		yoff := (scaled.Bounds().Dy() - h) / 2

		tr := image.Rect(0, 0, w, h)
		target := image.NewRGBA(tr)
		draw.Draw(target, tr, scaled, image.Pt(xoff, yoff), draw.Src)
		out = target
	} else {
		out = resize.Thumbnail(uint(w), uint(h), img, resize.Lanczos3)
		if err != nil {
			return -1, -1, err
		}
	}

	if err = writeFile(out, string(dst)); err != nil {
		logger.WithError(err).Error("Failed to encode and write image")
		return -1, -1, err
	}

	return out.Bounds().Max.X, out.Bounds().Max.Y, nil
}