mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2024-11-16 03:02:45 +00:00
237 lines
5.4 KiB
Go
237 lines
5.4 KiB
Go
package httputil
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"io"
|
|
"mime"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/diamondburned/cchat-gtk/internal/gts"
|
|
"github.com/diamondburned/cchat-gtk/internal/log"
|
|
"github.com/diamondburned/cchat-gtk/internal/ui/primitives"
|
|
"github.com/diamondburned/imgutil"
|
|
"github.com/gotk3/gotk3/cairo"
|
|
"github.com/gotk3/gotk3/gdk"
|
|
"github.com/gotk3/gotk3/gtk"
|
|
"github.com/pkg/errors"
|
|
)
|
|
|
|
// bufferPool provides a sync.Pool of *bufio.Writers. This is used to reduce the
|
|
// amount of cgo calls, by writing bytes in larger chunks.
|
|
//
|
|
// Technically, httpcache already wraps its cached reader around a bufio.Reader,
|
|
// but we have no control over the buffer size.
|
|
var bufferPool = sync.Pool{
|
|
New: func() interface{} {
|
|
// Allocate a 512KB buffer by default.
|
|
const defaultBufSz = 512 * 1024
|
|
|
|
return bufio.NewWriterSize(nil, defaultBufSz)
|
|
},
|
|
}
|
|
|
|
func bufferedWriter(w io.Writer) *bufio.Writer {
|
|
buf := bufferPool.Get().(*bufio.Writer)
|
|
buf.Reset(w)
|
|
return buf
|
|
}
|
|
|
|
func returnBufferedWriter(buf *bufio.Writer) {
|
|
// Unreference the internal reader.
|
|
buf.Reset(nil)
|
|
bufferPool.Put(buf)
|
|
}
|
|
|
|
type ImageContainer interface {
|
|
primitives.Connector
|
|
|
|
SetFromPixbuf(*gdk.Pixbuf)
|
|
SetFromAnimation(*gdk.PixbufAnimation)
|
|
GetSizeRequest() (w, h int)
|
|
}
|
|
|
|
type SurfaceContainer interface {
|
|
ImageContainer
|
|
GetScaleFactor() int
|
|
SetFromSurface(*cairo.Surface)
|
|
}
|
|
|
|
var (
|
|
_ ImageContainer = (*gtk.Image)(nil)
|
|
_ SurfaceContainer = (*gtk.Image)(nil)
|
|
)
|
|
|
|
type surfaceWrapper struct {
|
|
SurfaceContainer
|
|
scale int
|
|
}
|
|
|
|
func (wrapper *surfaceWrapper) SetFromPixbuf(pb *gdk.Pixbuf) {
|
|
surface, _ := gdk.CairoSurfaceCreateFromPixbuf(pb, wrapper.scale, nil)
|
|
wrapper.SetFromSurface(surface)
|
|
}
|
|
|
|
// AsyncImage loads an image. This method uses the cache. It prefers loading
|
|
// SetFromSurface over SetFromPixbuf, but will fallback if needed be.
|
|
func AsyncImage(ctx context.Context,
|
|
img ImageContainer, imageURL string, procs ...imgutil.Processor) {
|
|
|
|
if imageURL == "" {
|
|
return
|
|
}
|
|
|
|
w, h := img.GetSizeRequest()
|
|
scale := 1
|
|
|
|
surfaceContainer, canSurface := img.(SurfaceContainer)
|
|
if canSurface {
|
|
scale = surfaceContainer.GetScaleFactor()
|
|
}
|
|
|
|
ctx = primitives.HandleDestroyCtx(ctx, img)
|
|
|
|
go func() {
|
|
// Try and guess the MIME type from the URL.
|
|
mimeType := mime.TypeByExtension(urlExt(imageURL))
|
|
|
|
r, err := get(ctx, imageURL, true)
|
|
if err != nil {
|
|
log.Error(errors.Wrap(err, "failed to GET"))
|
|
return
|
|
}
|
|
defer r.Body.Close()
|
|
|
|
// Try and use the image type from the MIME header over the type from
|
|
// the URL, as it is more reliable.
|
|
if mime := mimeFromHeaders(r.Header); mime != "" {
|
|
mimeType = mime
|
|
}
|
|
|
|
_, fileType := path.Split(mimeType) // abuse split "a/b" to get b
|
|
|
|
isGIF := fileType == "gif"
|
|
if isGIF {
|
|
canSurface = false
|
|
scale = 1
|
|
}
|
|
|
|
// Only bother with this if we even have HiDPI. We also can't use a
|
|
// Surface for a GIF.
|
|
if canSurface && scale > 1 {
|
|
img = &surfaceWrapper{surfaceContainer, scale}
|
|
}
|
|
|
|
l, err := gdk.PixbufLoaderNewWithType(fileType)
|
|
if err != nil {
|
|
log.Error(errors.Wrapf(err, "failed to make PixbufLoader type %q", fileType))
|
|
return
|
|
}
|
|
|
|
l.Connect("size-prepared", func(l *gdk.PixbufLoader, imgW, imgH int) {
|
|
w, h = imgutil.MaxSize(imgW, imgH, w, h)
|
|
if w != imgW || h != imgH || scale > 1 {
|
|
l.SetSize(w*scale, h*scale)
|
|
}
|
|
})
|
|
|
|
load := loadFn(ctx, img, isGIF)
|
|
l.Connect("area-prepared", load)
|
|
l.Connect("area-updated", load)
|
|
|
|
// Borrow a buffered writer and return it at the end.
|
|
bufWriter := bufferedWriter(l)
|
|
defer returnBufferedWriter(bufWriter)
|
|
|
|
if err := downloadImage(r.Body, bufWriter, procs, isGIF); err != nil {
|
|
log.Error(errors.Wrapf(err, "failed to download %q", imageURL))
|
|
// Force close after downloading.
|
|
}
|
|
|
|
if err := bufWriter.Flush(); err != nil {
|
|
log.Error(errors.Wrapf(err, "failed to flush writer for %q", imageURL))
|
|
// Force close after downloading.
|
|
}
|
|
|
|
if err := l.Close(); err != nil {
|
|
log.Error(errors.Wrapf(err, "failed to close pixbuf loader for %q", imageURL))
|
|
}
|
|
}()
|
|
}
|
|
|
|
func urlExt(anyURL string) string {
|
|
u, err := url.Parse(anyURL)
|
|
if err != nil {
|
|
return path.Ext(strings.SplitN(anyURL, "?", 1)[0])
|
|
}
|
|
|
|
return path.Ext(u.Path)
|
|
}
|
|
|
|
func mimeFromHeaders(headers http.Header) string {
|
|
cType := headers.Get("Content-Type")
|
|
if cType == "" {
|
|
return ""
|
|
}
|
|
media, _, err := mime.ParseMediaType(cType)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
return media
|
|
}
|
|
|
|
func loadFn(ctx context.Context, img ImageContainer, isGIF bool) func(l *gdk.PixbufLoader) {
|
|
var pixbuf interface{}
|
|
|
|
return func(l *gdk.PixbufLoader) {
|
|
if pixbuf == nil {
|
|
if !isGIF {
|
|
pixbuf, _ = l.GetPixbuf()
|
|
} else {
|
|
pixbuf, _ = l.GetAnimation()
|
|
}
|
|
}
|
|
|
|
switch pixbuf := pixbuf.(type) {
|
|
case *gdk.Pixbuf:
|
|
execIfCtx(ctx, func() { img.SetFromPixbuf(pixbuf) })
|
|
case *gdk.PixbufAnimation:
|
|
execIfCtx(ctx, func() { img.SetFromAnimation(pixbuf) })
|
|
}
|
|
}
|
|
}
|
|
|
|
func execIfCtx(ctx context.Context, fn func()) {
|
|
gts.ExecLater(func() {
|
|
if ctx.Err() == nil {
|
|
fn()
|
|
}
|
|
})
|
|
}
|
|
|
|
func downloadImage(src io.Reader, dst io.Writer, p []imgutil.Processor, isGIF bool) error {
|
|
var err error
|
|
|
|
// If we have processors, then write directly in there.
|
|
if len(p) > 0 {
|
|
if !isGIF {
|
|
err = imgutil.ProcessStream(dst, src, p)
|
|
} else {
|
|
err = imgutil.ProcessAnimationStream(dst, src, p)
|
|
}
|
|
} else {
|
|
// Else, directly copy the body over.
|
|
_, err = io.Copy(dst, src)
|
|
}
|
|
|
|
if err != nil {
|
|
return errors.Wrap(err, "failed to process image")
|
|
}
|
|
|
|
return nil
|
|
}
|