177 lines
5.9 KiB
Nim
177 lines
5.9 KiB
Nim
import os, sets, random, httpClient, json, strutils, strformat, options, deques, times
|
|
from lenientops import `*`
|
|
import gintro/[gdkpixbuf, gobject]
|
|
import common
|
|
|
|
const
|
|
supportedExts = @[".png", ".jpg", ".jpeg"]
|
|
foxesUrl = "https://randomfox.ca/floof/"
|
|
inspiroUrl = "http://inspirobot.me/api?generate=true"
|
|
|
|
type
|
|
FileOpResult* = object of OpResult
|
|
file*: string
|
|
|
|
ImageProvider* = ref object of RootObj
|
|
## Manages images that should be displayed
|
|
verbose: bool ## Additional logging for the image provider
|
|
mode* : Mode ## Selects the API that is used to get images
|
|
path*: Option[string] ## Path on the local file syetem that will be used in `file` mode
|
|
exts: HashSet[string] ## Allowed extensions that the `file` mode will display
|
|
|
|
var
|
|
client = newHttpClient() ## For loading images from the web
|
|
tmpDir = getTempDir() / "randopix"
|
|
tmpFile = tmpDir / "tmp.png"
|
|
fileList = initDeque[string]()
|
|
|
|
########################
|
|
# Constructors
|
|
########################
|
|
|
|
proc newImageProvider(verbose: bool, mode: Mode, path: Option[string]): ImageProvider =
|
|
ImageProvider(verbose: verbose, mode: mode, path: path, exts: supportedExts.toHashSet)
|
|
|
|
proc newImageProvider*(verbose: bool): ImageProvider =
|
|
newImageProvider(verbose, Mode.None, none(string))
|
|
|
|
proc newImageProvider*(verbose: bool, path: string): ImageProvider =
|
|
newImageProvider(verbose, Mode.None, some(path))
|
|
|
|
proc newImageProvider*(verbose: bool, mode: Mode): ImageProvider =
|
|
newImageProvider(verbose, mode, none(string))
|
|
|
|
proc newImageProvider*(verbose: bool, mode: Mode, path: string): ImageProvider =
|
|
newImageProvider(verbose, mode, some(path))
|
|
|
|
proc newFileOpResultError(msg: string): FileOpResult =
|
|
FileOpResult(success: false, errorMsg: msg)
|
|
|
|
proc newFileOpResult(file: string): FileOpResult =
|
|
FileOpResult(success: true, file: file)
|
|
|
|
########################
|
|
# Utilities
|
|
########################
|
|
|
|
proc log(ip: ImageProvider, things: varargs[string, `$`]) =
|
|
if ip.verbose:
|
|
echo things.join()
|
|
|
|
func calcImageSize(maxWidth, maxHeight, imgWidth, imgHeight: int): tuple[width: int, height: int] =
|
|
## Calculate the best fit for an image on the give screen size.
|
|
## This should keep the image aspect ratio
|
|
let
|
|
ratioMax = maxWidth / maxHeight
|
|
ratioImg = imgWidth / imgHeight
|
|
if (ratioMax > ratioImg):
|
|
result.width = (imgWidth * (maxHeight / imgHeight)).toInt
|
|
result.height = maxHeight
|
|
else:
|
|
result.width = maxWidth
|
|
result.height = (imgHeight * (maxWidth / imgWidth)).toInt
|
|
|
|
########################
|
|
# Image Provider procs
|
|
########################
|
|
|
|
proc getFox(ip: ImageProvider): FileOpResult =
|
|
## Download image from the fox API
|
|
try:
|
|
let urlData = client.getContent(foxesUrl)
|
|
let info = parseJson(urlData)
|
|
let imageData = client.getContent(info["image"].getStr)
|
|
let dlFile = fmt"{tmpFile}.download"
|
|
writeFile(dlFile, imageData)
|
|
return newFileOpResult(dlFile)
|
|
except JsonParsingError:
|
|
ip.log fmt"Error while fetching from fox API: {getCurrentExceptionMsg()}"
|
|
return newFileOpResultError("Json parsing error")
|
|
except KeyError:
|
|
ip.log fmt"No image in downloaded data: {getCurrentExceptionMsg()}"
|
|
return newFileOpResultError("No image from API")
|
|
|
|
proc getInspiro(ip: ImageProvider): FileOpResult =
|
|
## Download and save image from the inspiro API
|
|
try:
|
|
let imageUrl = client.getContent(inspiroUrl)
|
|
ip.log fmt"Downloading inspiro image from: '{imageUrl}'"
|
|
let imageData = client.getContent(imageUrl)
|
|
let dlFile = fmt"{tmpFile}.download"
|
|
writeFile(dlFile,imageData)
|
|
return newFileOpResult(dlFile)
|
|
except:
|
|
ip.log fmt"Unexpected error while downloading: {getCurrentExceptionMsg()}"
|
|
return newFileOpResultError(getCurrentExceptionMsg())
|
|
|
|
proc getLocalFile(ip: var ImageProvider): FileOpResult =
|
|
## Provide an image from a local folder
|
|
|
|
# First, check if there are still images left to be loaded.
|
|
# If not reread all files from the path
|
|
if fileList.len == 0:
|
|
var tmp: seq[string]
|
|
var split: tuple[dir, name, ext: string]
|
|
for file in walkDirRec(ip.path.get):
|
|
split = splitFile(file)
|
|
if ip.exts.contains(split.ext):
|
|
tmp.add($file)
|
|
|
|
ip.log fmt"Loaded {tmp.len} files"
|
|
shuffle(tmp)
|
|
for file in tmp:
|
|
fileList.addLast(file)
|
|
if fileList.len == 0:
|
|
return newFileOpResultError("No files found")
|
|
|
|
let next = fileList.popFirst()
|
|
# Remove the current file after
|
|
result = newFileOpResult(next)
|
|
|
|
proc getFileName(ip: var ImageProvider): FileOpResult =
|
|
## Get the temporary file name of the next file to display
|
|
case ip.mode
|
|
of Mode.File:
|
|
return ip.getLocalFile()
|
|
of Mode.Foxes:
|
|
return ip.getFox()
|
|
of Mode.Inspiro:
|
|
return ip.getInspiro()
|
|
else:
|
|
return newFileOpResultError("Not implemented")
|
|
|
|
########################
|
|
# Exported procs
|
|
########################
|
|
|
|
proc next*(ip: var ImageProvider, maxWidth, maxHeight: int): FileOpResult =
|
|
## Uses the image provider to get a new image ready to display.
|
|
## `width` and `height` should be the size of the window.
|
|
if ip.mode == Mode.None:
|
|
return newFileOpResultError("No mode active")
|
|
|
|
let op = ip.getFileName()
|
|
if not op.success: return op
|
|
|
|
var rawPixbuf = newPixbufFromFile(op.file)
|
|
# Resize the pixbuf to best fit on screen
|
|
let size = calcImageSize(maxWidth, maxHeight, rawPixbuf.width, rawPixbuf.height)
|
|
ip.log "Scale image to: ", size
|
|
let then = now()
|
|
var pixbuf = rawPixbuf.scaleSimple(size.width, size.height, InterpType.nearest)
|
|
let now = now()
|
|
ip.log "Image scaled. Time: ", (now - then).inMilliseconds, "ms"
|
|
# The pixbuf is written to disk and loaded again once because
|
|
# directly setting the image from a pixbuf will leak memory
|
|
let saved = pixbuf.savev(tmpFile, "png", @[])
|
|
if not saved:
|
|
return newFileOpResultError("Error while saving temporary image")
|
|
|
|
# GTK pixbuf leaks memory when not manually decreasing reference count
|
|
pixbuf.unref()
|
|
rawPixbuf.unref()
|
|
|
|
newFileOpResult(tmpFile)
|
|
|
|
createDir(tmpDir)
|
|
randomize() |