gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

image.go (5597B)


      1 // GoToSocial
      2 // Copyright (C) GoToSocial Authors admin@gotosocial.org
      3 // SPDX-License-Identifier: AGPL-3.0-or-later
      4 //
      5 // This program is free software: you can redistribute it and/or modify
      6 // it under the terms of the GNU Affero General Public License as published by
      7 // the Free Software Foundation, either version 3 of the License, or
      8 // (at your option) any later version.
      9 //
     10 // This program is distributed in the hope that it will be useful,
     11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
     12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
     13 // GNU Affero General Public License for more details.
     14 //
     15 // You should have received a copy of the GNU Affero General Public License
     16 // along with this program.  If not, see <http://www.gnu.org/licenses/>.
     17 
     18 package media
     19 
     20 import (
     21 	"bufio"
     22 	"image"
     23 	"image/color"
     24 	"image/draw"
     25 	"image/jpeg"
     26 	"image/png"
     27 	"io"
     28 	"sync"
     29 
     30 	"github.com/buckket/go-blurhash"
     31 	"github.com/disintegration/imaging"
     32 	"github.com/superseriousbusiness/gotosocial/internal/iotools"
     33 
     34 	// import to init webp encode/decoding.
     35 	_ "golang.org/x/image/webp"
     36 )
     37 
     38 var (
     39 	// pngEncoder provides our global PNG encoding with
     40 	// specified compression level, and memory pooled buffers.
     41 	pngEncoder = png.Encoder{
     42 		CompressionLevel: png.DefaultCompression,
     43 		BufferPool:       &pngEncoderBufferPool{},
     44 	}
     45 
     46 	// jpegBufferPool is a memory pool of byte buffers for JPEG encoding.
     47 	jpegBufferPool = sync.Pool{
     48 		New: func() any {
     49 			return bufio.NewWriter(nil)
     50 		},
     51 	}
     52 )
     53 
     54 // gtsImage is a thin wrapper around the standard library image
     55 // interface to provide our own useful helper functions for image
     56 // size and aspect ratio calculations, streamed encoding to various
     57 // types, and creating reduced size thumbnail images.
     58 type gtsImage struct{ image image.Image }
     59 
     60 // blankImage generates a blank image of given dimensions.
     61 func blankImage(width int, height int) *gtsImage {
     62 	// create a rectangle with the same dimensions as the video
     63 	img := image.NewRGBA(image.Rect(0, 0, width, height))
     64 
     65 	// fill the rectangle with our desired fill color.
     66 	draw.Draw(img, img.Bounds(), &image.Uniform{
     67 		color.RGBA{42, 43, 47, 0},
     68 	}, image.Point{}, draw.Src)
     69 
     70 	return &gtsImage{image: img}
     71 }
     72 
     73 // decodeImage will decode image from reader stream and return image wrapped in our own gtsImage{} type.
     74 func decodeImage(r io.Reader, opts ...imaging.DecodeOption) (*gtsImage, error) {
     75 	img, err := imaging.Decode(r, opts...)
     76 	if err != nil {
     77 		return nil, err
     78 	}
     79 	return &gtsImage{image: img}, nil
     80 }
     81 
     82 // Width returns the image width in pixels.
     83 func (m *gtsImage) Width() uint32 {
     84 	return uint32(m.image.Bounds().Size().X)
     85 }
     86 
     87 // Height returns the image height in pixels.
     88 func (m *gtsImage) Height() uint32 {
     89 	return uint32(m.image.Bounds().Size().Y)
     90 }
     91 
     92 // Size returns the total number of image pixels.
     93 func (m *gtsImage) Size() uint64 {
     94 	return uint64(m.image.Bounds().Size().X) *
     95 		uint64(m.image.Bounds().Size().Y)
     96 }
     97 
     98 // AspectRatio returns the image ratio of width:height.
     99 func (m *gtsImage) AspectRatio() float32 {
    100 	return float32(m.image.Bounds().Size().X) /
    101 		float32(m.image.Bounds().Size().Y)
    102 }
    103 
    104 // Thumbnail returns a small sized copy of gtsImage{}, limited to 512x512 if not small enough.
    105 func (m *gtsImage) Thumbnail() *gtsImage {
    106 	const (
    107 		// max thumb
    108 		// dimensions.
    109 		maxWidth  = 512
    110 		maxHeight = 512
    111 	)
    112 
    113 	// Check the receiving image is within max thumnail bounds.
    114 	if m.Width() <= maxWidth && m.Height() <= maxHeight {
    115 		return &gtsImage{image: imaging.Clone(m.image)}
    116 	}
    117 
    118 	// Image is too large, needs to be resized to thumbnail max.
    119 	img := imaging.Fit(m.image, maxWidth, maxHeight, imaging.Linear)
    120 	return &gtsImage{image: img}
    121 }
    122 
    123 // Blurhash calculates the blurhash for the receiving image data.
    124 func (m *gtsImage) Blurhash() (string, error) {
    125 	// for generating blurhashes, it's more cost effective to
    126 	// lose detail since it's blurry, so make a tiny version.
    127 	tiny := imaging.Resize(m.image, 32, 0, imaging.NearestNeighbor)
    128 
    129 	// Encode blurhash from resized version
    130 	return blurhash.Encode(4, 3, tiny)
    131 }
    132 
    133 // ToJPEG creates a new streaming JPEG encoder from receiving image, and a size ptr
    134 // which stores the number of bytes written during the image encoding process.
    135 func (m *gtsImage) ToJPEG(opts *jpeg.Options) io.Reader {
    136 	return iotools.StreamWriteFunc(func(w io.Writer) error {
    137 		// Get encoding buffer
    138 		bw := getJPEGBuffer(w)
    139 
    140 		// Encode JPEG to buffered writer.
    141 		err := jpeg.Encode(bw, m.image, opts)
    142 
    143 		// Replace buffer.
    144 		//
    145 		// NOTE: jpeg.Encode() already
    146 		// performs a bufio.Writer.Flush().
    147 		putJPEGBuffer(bw)
    148 
    149 		return err
    150 	})
    151 }
    152 
    153 // ToPNG creates a new streaming PNG encoder from receiving image, and a size ptr
    154 // which stores the number of bytes written during the image encoding process.
    155 func (m *gtsImage) ToPNG() io.Reader {
    156 	return iotools.StreamWriteFunc(func(w io.Writer) error {
    157 		return pngEncoder.Encode(w, m.image)
    158 	})
    159 }
    160 
    161 // getJPEGBuffer fetches a reset JPEG encoding buffer from global JPEG buffer pool.
    162 func getJPEGBuffer(w io.Writer) *bufio.Writer {
    163 	buf, _ := jpegBufferPool.Get().(*bufio.Writer)
    164 	buf.Reset(w)
    165 	return buf
    166 }
    167 
    168 // putJPEGBuffer resets the given bufio writer and places in global JPEG buffer pool.
    169 func putJPEGBuffer(buf *bufio.Writer) {
    170 	buf.Reset(nil)
    171 	jpegBufferPool.Put(buf)
    172 }
    173 
    174 // pngEncoderBufferPool implements png.EncoderBufferPool.
    175 type pngEncoderBufferPool sync.Pool
    176 
    177 func (p *pngEncoderBufferPool) Get() *png.EncoderBuffer {
    178 	buf, _ := (*sync.Pool)(p).Get().(*png.EncoderBuffer)
    179 	return buf
    180 }
    181 
    182 func (p *pngEncoderBufferPool) Put(buf *png.EncoderBuffer) {
    183 	(*sync.Pool)(p).Put(buf)
    184 }