giffer

Create .gif images from sites like youtube.com
Log | Files | Refs | LICENSE

main.go (8823B)


      1 package main
      2 
      3 import (
      4 	"bytes"
      5 	"image"
      6 	"image/color"
      7 	"image/gif"
      8 	"log"
      9 	"os"
     10 	"path/filepath"
     11 	"strconv"
     12 	"time"
     13 
     14 	"gioui.org/app"
     15 	"gioui.org/font/gofont"
     16 	"gioui.org/io/system"
     17 	"gioui.org/layout"
     18 	l "gioui.org/layout"
     19 	"gioui.org/op"
     20 	"gioui.org/op/paint"
     21 	"gioui.org/unit"
     22 	"gioui.org/widget"
     23 	"gioui.org/widget/material"
     24 	m "gioui.org/widget/material"
     25 	c "gioui.org/x/component"
     26 	"github.com/ncruces/zenity"
     27 )
     28 
     29 func main() {
     30 	go func() {
     31 		ui := UI{
     32 			Window: app.NewWindow(
     33 				app.Title("Giffer"),
     34 				app.MinSize(unit.Dp(800), unit.Dp(425)),
     35 			),
     36 			Th: material.NewTheme(gofont.Collection()),
     37 			Giffer: Giffer{
     38 				Store: &gifdb{
     39 					Dir: filepath.Join(os.TempDir(), "giffer"),
     40 				},
     41 			},
     42 		}
     43 		if err := ui.Loop(); err != nil {
     44 			log.Fatalf("error: %v", err)
     45 		}
     46 		os.Exit(0)
     47 	}()
     48 	app.Main()
     49 }
     50 
     51 type (
     52 	C = layout.Context
     53 	D = layout.Dimensions
     54 )
     55 
     56 type UI struct {
     57 	*app.Window
     58 	Giffer     Giffer
     59 	Th         *m.Theme
     60 	Form       Form
     61 	GifPlayer  GifPlayer
     62 	Processing bool
     63 	cache      *PreparedGif
     64 	done       chan *PreparedGif
     65 }
     66 
     67 // PreparedGif helper wraps a Gif with FPS metadata.
     68 // This is needed because Gif itself cannot be relied upon to contain proper FPS data in it's delay
     69 // slice.
     70 type PreparedGif struct {
     71 	*gif.GIF
     72 	FPS float64
     73 }
     74 
     75 // Loop runs the event loop until terminated.
     76 func (ui *UI) Loop() error {
     77 	var (
     78 		ops    op.Ops
     79 		events = ui.Window.Events()
     80 	)
     81 	ui.Init()
     82 	for event := range events {
     83 		switch event := (event).(type) {
     84 		case system.DestroyEvent:
     85 			return event.Err
     86 		case system.FrameEvent:
     87 			gtx := layout.NewContext(&ops, event)
     88 			ui.Update(gtx)
     89 			ui.Layout(gtx)
     90 			event.Frame(gtx.Ops)
     91 		}
     92 	}
     93 	return nil
     94 }
     95 
     96 func (ui *UI) Init() {
     97 	ui.Form.FPS.SetText("12")
     98 	ui.Form.Width.SetText("400")
     99 	ui.Form.Height.SetText("350")
    100 	ui.Form.Start.SetText("0")
    101 	ui.Form.End.SetText("3")
    102 	ui.done = make(chan *PreparedGif)
    103 }
    104 
    105 func (ui *UI) Update(gtx C) {
    106 	if ui.Form.SaveBtn.Clicked() && ui.cache != nil {
    107 		path, err := zenity.SelectFileSave(zenity.Title("Save Gif"), zenity.ConfirmOverwrite())
    108 		if err != nil {
    109 			log.Printf("error: selecting save file: %v", err)
    110 		}
    111 		savef, err := os.OpenFile(path, os.O_CREATE|os.O_RDWR, 0664)
    112 		if err != nil {
    113 			log.Printf("error: opening save file: %v", err)
    114 		}
    115 		defer func() { go savef.Close() }()
    116 		if err := gif.EncodeAll(savef, ui.cache.GIF); err != nil {
    117 			log.Printf("error: saving gif to %q: %v", path, err)
    118 		}
    119 		ui.cache = nil
    120 	}
    121 	if ui.Form.SubmitBtn.Clicked() {
    122 		ui.GifPlayer.Clear()
    123 		ui.Processing = true
    124 		var (
    125 			url  = ui.Form.URL.Text()
    126 			fuzz = 0
    127 		)
    128 		// TODO(jfm): show as validation errors.
    129 		start, err := strconv.ParseFloat(ui.Form.Start.Text(), 64)
    130 		if err != nil {
    131 			log.Printf("error: start must be a floating point number")
    132 		}
    133 		end, err := strconv.ParseFloat(ui.Form.End.Text(), 64)
    134 		if err != nil {
    135 			log.Printf("error: end must be a floating point number")
    136 		}
    137 		fps, err := strconv.ParseFloat(ui.Form.FPS.Text(), 64)
    138 		if err != nil {
    139 			log.Printf("error: fps must be a floating point number")
    140 		}
    141 		width, err := strconv.Atoi(ui.Form.Width.Text())
    142 		if err != nil {
    143 			log.Printf("error: width must be an integer number")
    144 		}
    145 		height, err := strconv.Atoi(ui.Form.Height.Text())
    146 		if err != nil {
    147 			log.Printf("error: height must be an integer number")
    148 		}
    149 		go func() {
    150 			g, err := ui.Giffer.GififyURL(
    151 				url,
    152 				start,
    153 				end,
    154 				fps,
    155 				width,
    156 				height,
    157 				fuzz,
    158 			)
    159 			if err != nil {
    160 				log.Printf("error: fetching gif: %v", err)
    161 				return
    162 			}
    163 			img, err := gif.DecodeAll(bytes.NewReader(g.Data))
    164 			if err != nil {
    165 				log.Printf("error: decoding gif: %v", err)
    166 				return
    167 			}
    168 			ui.GifPlayer.FPS = fps
    169 			ui.done <- &PreparedGif{
    170 				GIF: img,
    171 				FPS: fps,
    172 			}
    173 		}()
    174 	}
    175 	select {
    176 	case img := <-ui.done:
    177 		ui.cache = img
    178 		ui.Processing = false
    179 		ui.GifPlayer.Load(img)
    180 	default:
    181 	}
    182 }
    183 
    184 func (ui *UI) Layout(gtx C) D {
    185 	return layout.Stack{}.Layout(
    186 		gtx,
    187 		layout.Stacked(func(gtx C) D {
    188 			return l.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx C) D {
    189 				return l.Flex{
    190 					Axis: l.Horizontal,
    191 				}.Layout(
    192 					gtx,
    193 					l.Flexed(1, func(gtx C) D {
    194 						return l.Center.Layout(gtx, func(gtx C) D {
    195 							var (
    196 								cs  = &gtx.Constraints
    197 								max = gtx.Dp(400)
    198 							)
    199 							if cs.Max.X > max {
    200 								cs.Max.X = max
    201 							}
    202 							if ui.Processing {
    203 								gtx.Queue = nil
    204 							}
    205 							return l.Flex{
    206 								Axis: l.Vertical,
    207 							}.Layout(
    208 								gtx,
    209 								l.Rigid(func(gtx C) D {
    210 									return ui.Form.LayoutFields(gtx, ui.Th)
    211 								}),
    212 								l.Flexed(1, func(gtx C) D {
    213 									return D{Size: gtx.Constraints.Max}
    214 								}),
    215 								l.Rigid(func(gtx C) D {
    216 									return l.Flex{
    217 										Axis: l.Horizontal,
    218 									}.Layout(
    219 										gtx,
    220 										l.Rigid(func(gtx C) D {
    221 											return m.Button(ui.Th, &ui.Form.SubmitBtn, "Create").
    222 												Layout(gtx)
    223 										}),
    224 										l.Rigid(func(gtx C) D {
    225 											return D{Size: image.Point{X: 10}}
    226 										}),
    227 										l.Rigid(func(gtx C) D {
    228 											if ui.cache == nil {
    229 												gtx.Queue = nil
    230 											}
    231 											return m.Button(ui.Th, &ui.Form.SaveBtn, "Save").
    232 												Layout(gtx)
    233 										}),
    234 									)
    235 								}),
    236 							)
    237 						})
    238 					}),
    239 					l.Rigid(func(gtx C) D {
    240 						return D{Size: image.Point{X: 10}}
    241 					}),
    242 					l.Flexed(1, func(gtx C) D {
    243 						return ui.GifPlayer.Layout(gtx)
    244 					}),
    245 				)
    246 			})
    247 		}),
    248 		layout.Expanded(func(gtx C) D {
    249 			if !ui.Processing {
    250 				return D{}
    251 			}
    252 			return c.Rect{
    253 				Color: color.NRGBA{A: 100},
    254 				Size:  gtx.Constraints.Max,
    255 			}.Layout(gtx)
    256 		}),
    257 		layout.Expanded(func(gtx C) D {
    258 			if !ui.Processing {
    259 				return D{}
    260 			}
    261 			cs := &gtx.Constraints
    262 			cs.Max.X = 50
    263 			cs.Max.Y = 50
    264 			return l.Center.Layout(gtx, func(gtx C) D {
    265 				return m.Loader(ui.Th).Layout(gtx)
    266 			})
    267 		}),
    268 	)
    269 }
    270 
    271 // Form holds state for form inputs.
    272 type Form struct {
    273 	URL       c.TextField
    274 	Start     c.TextField
    275 	End       c.TextField
    276 	Width     c.TextField
    277 	Height    c.TextField
    278 	FPS       c.TextField
    279 	SubmitBtn widget.Clickable
    280 	SaveBtn   widget.Clickable
    281 }
    282 
    283 func (f *Form) LayoutFields(gtx C, th *m.Theme) D {
    284 	return l.Flex{
    285 		Axis: l.Vertical,
    286 	}.Layout(
    287 		gtx,
    288 		l.Rigid(func(gtx C) D {
    289 			return f.URL.Layout(gtx, th, "url")
    290 		}),
    291 		l.Rigid(func(gtx C) D {
    292 			return f.Start.Layout(gtx, th, "start (seconds)")
    293 		}),
    294 		l.Rigid(func(gtx C) D {
    295 			return f.End.Layout(gtx, th, "end (seconds)")
    296 		}),
    297 		l.Rigid(func(gtx C) D {
    298 			return f.Width.Layout(gtx, th, "width (pixels)")
    299 		}),
    300 		l.Rigid(func(gtx C) D {
    301 			return f.Height.Layout(gtx, th, "height (pixels)")
    302 		}),
    303 		l.Rigid(func(gtx C) D {
    304 			return f.FPS.Layout(gtx, th, "fps (integer)")
    305 		}),
    306 		l.Rigid(func(gtx C) D {
    307 			return D{Size: image.Point{Y: 10}}
    308 		}),
    309 	)
    310 }
    311 
    312 func (f *Form) LayoutActions(gtx C, th *m.Theme) D {
    313 	return l.Flex{
    314 		Axis: l.Horizontal,
    315 	}.Layout(
    316 		gtx,
    317 		l.Rigid(func(gtx C) D {
    318 			return m.Button(th, &f.SubmitBtn, "Create").Layout(gtx)
    319 		}),
    320 		l.Rigid(func(gtx C) D {
    321 			return D{Size: image.Point{X: 10}}
    322 		}),
    323 		l.Rigid(func(gtx C) D {
    324 			return m.Button(th, &f.SaveBtn, "Save").Layout(gtx)
    325 		}),
    326 	)
    327 }
    328 
    329 // GifPlayer animates through a series of frames.
    330 //
    331 // TODO(jfm): Fix artifacting from the ImageStack implementation.
    332 // TODO(jfm): Properly crop gif.
    333 type GifPlayer struct {
    334 	Frames []paint.ImageOp
    335 	Cursor int
    336 	FPS    float64
    337 	since  time.Time
    338 	img    widget.Image
    339 }
    340 
    341 // Load a Gif image to render.
    342 func (g *GifPlayer) Load(src *PreparedGif) {
    343 	g.FPS = float64(src.FPS)
    344 	g.Frames = make([]paint.ImageOp, len(src.Image))
    345 	for ii := range src.Image {
    346 		s := ImageStack{Config: src.Config}
    347 		for jj := ii; jj >= 0; jj-- {
    348 			s.Stack = append(s.Stack, src.Image[jj])
    349 		}
    350 		g.Frames[ii] = paint.NewImageOp(s)
    351 	}
    352 }
    353 
    354 // Clear the player state.
    355 // Stops playing any current gif.
    356 func (g *GifPlayer) Clear() {
    357 	g.Frames = g.Frames[:]
    358 	g.Cursor = 0
    359 	g.FPS = 0
    360 }
    361 
    362 // Ready if the next frame is ready to be displayed.
    363 func (g *GifPlayer) Ready(gtx C) bool {
    364 	var (
    365 		now     = gtx.Now
    366 		since   = g.since
    367 		latency = time.Second / time.Duration(g.FPS)
    368 	)
    369 	return now.Sub(since).Milliseconds() >= latency.Milliseconds()
    370 }
    371 
    372 // Next loads the next frame in the series.
    373 func (g *GifPlayer) Next(gtx C) {
    374 	defer func() {
    375 		g.Cursor++
    376 		if g.Cursor > len(g.Frames)-1 {
    377 			g.Cursor = 0
    378 		}
    379 		g.since = gtx.Now
    380 	}()
    381 	g.img.Src = g.Frames[g.Cursor]
    382 }
    383 
    384 // Current returns the current frame.
    385 func (g *GifPlayer) Current() paint.ImageOp {
    386 	return g.Frames[g.Cursor]
    387 }
    388 
    389 func (g *GifPlayer) Layout(gtx C) D {
    390 	if g == nil || len(g.Frames) == 0 {
    391 		return c.Rect{
    392 			Size: image.Point{
    393 				X: gtx.Constraints.Max.X,
    394 				Y: gtx.Constraints.Max.Y,
    395 			},
    396 			Color: color.NRGBA{A: 100},
    397 		}.Layout(gtx)
    398 	}
    399 	op.InvalidateOp{}.Add(gtx.Ops)
    400 	if g.Ready(gtx) {
    401 		g.Next(gtx)
    402 	}
    403 	g.img.Fit = widget.Fill
    404 	return g.img.Layout(gtx)
    405 }