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 >sImage{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 >sImage{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 >sImage{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 >sImage{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 }