diff --git a/DESCRIPTION b/DESCRIPTION index a1a2370..d72cec6 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,7 +1,7 @@ Package: gifski Type: Package Title: Highest Quality GIF Encoder -Version: 0.8.6 +Version: 0.8.6.1 Authors@R: c( person("Jeroen", "Ooms", ,"jeroen@berkeley.edu", role = c("aut", "cre"), comment = c(ORCID = "0000-0002-4035-0289")), @@ -15,7 +15,7 @@ BugReports: https://github.com/r-rust/gifski/issues SystemRequirements: Cargo (rustc package manager) Encoding: UTF-8 LazyData: true -RoxygenNote: 6.0.1 +RoxygenNote: 6.1.0 Suggests: ggplot2, gapminder diff --git a/NAMESPACE b/NAMESPACE index 3ce13d0..eddab9b 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -1,5 +1,9 @@ # Generated by roxygen2: do not edit by hand export(gifski) +export(gifski_encoder_init) export(save_gif) +useDynLib(gifski,R_gifski_encoder_add_png) +useDynLib(gifski,R_gifski_encoder_finalize) +useDynLib(gifski,R_gifski_encoder_new) useDynLib(gifski,R_png_to_gif) diff --git a/R/gifski.R b/R/gifski.R index ed8e3c1..568e2f4 100644 --- a/R/gifski.R +++ b/R/gifski.R @@ -5,6 +5,7 @@ #' #' @export #' @rdname gifski +#' @family gifski #' @useDynLib gifski R_png_to_gif #' @param png_files vector of png files #' @param gif_file output gif file diff --git a/R/stream.R b/R/stream.R new file mode 100644 index 0000000..56d3b92 --- /dev/null +++ b/R/stream.R @@ -0,0 +1,41 @@ +#' Streaming Gifski Encoder +#' +#' Starts a stateful gif encoder and returns a closure to add png frames +#' to the gif. Upon completion, the caller has to invoke the closure one +#' more time with `png_file = NULL` to finalize the output. +#' +#' @export +#' @inheritParams gifski +#' @family gifski +#' @name streaming +#' @useDynLib gifski R_gifski_encoder_new +gifski_encoder_init <- function(gif_file = "animation.gif", width = 800, height = 600, loop = TRUE, delay = 1){ + gif_file <- normalizePath(gif_file, mustWork = FALSE) + if(!file.exists(dirname(gif_file))) + stop("Target directory does not exist:", dirname(gif_file)) + width <- as.integer(width) + height <- as.integer(height) + delay <- as.integer(delay * 100) + loop <- as.logical(loop) + ptr <- .Call(R_gifski_encoder_new, enc2utf8(gif_file), width, height, loop) + function(png_file){ + if(is.character(png_file)){ + gifski_add_png(ptr, png_file, delay) + } else if(is.null(png_file)){ + gifski_finalize(ptr) + } else { + stop("Invalid input: png_file must be a file path or NULL") + } + } +} + +#' @useDynLib gifski R_gifski_encoder_add_png +gifski_add_png <- function(ptr, png_file, delay){ + png_file <- normalizePath(png_file, mustWork = TRUE) + .Call(R_gifski_encoder_add_png, ptr, enc2utf8(png_file), delay) +} + +#' @useDynLib gifski R_gifski_encoder_finalize +gifski_finalize <- function(ptr){ + .Call(R_gifski_encoder_finalize, ptr) +} diff --git a/configure b/configure index 7b7a7de..2449c0f 100755 --- a/configure +++ b/configure @@ -8,6 +8,13 @@ if [ $? -eq 0 ]; then VERSION=$($CARGO --version) echo "Using $CARGO ($VERSION)" sed -e "s|@cargobin@|$CARGO|" src/Makevars.in > src/Makevars + + # CRAN forbids using $HOME during CMD check; try to override CARGO_HOME + if [ "$CARGO" == "/usr/bin/cargo" ] && [ "$_R_CHECK_SIZE_OF_TARBALL_" ]; then + if [ -z "CARGO_HOME" ] && [ ! -e "$HOME/.cargo" ]; then + sed -i.bak "s|#export|export|" src/Makevars + fi + fi exit 0 fi diff --git a/man/gifski.Rd b/man/gifski.Rd index 9c2eedc..44931c4 100644 --- a/man/gifski.Rd +++ b/man/gifski.Rd @@ -5,8 +5,8 @@ \alias{save_gif} \title{Gifski} \usage{ -gifski(png_files, gif_file = "animation.gif", width = 800, height = 600, - delay = 1, loop = TRUE, progress = TRUE) +gifski(png_files, gif_file = "animation.gif", width = 800, + height = 600, delay = 1, loop = TRUE, progress = TRUE) save_gif(expr, gif_file = "animation.gif", width = 800, height = 600, delay = 1, loop = TRUE, progress = TRUE, ...) @@ -67,3 +67,7 @@ gif_file <- file.path(tempdir(), 'gapminder.gif') save_gif(makeplot(), gif_file, 1280, 720, res = 144) utils::browseURL(gif_file)} } +\seealso{ +Other gifski: \code{\link{streaming}} +} +\concept{gifski} diff --git a/man/streaming.Rd b/man/streaming.Rd new file mode 100644 index 0000000..c4fc10a --- /dev/null +++ b/man/streaming.Rd @@ -0,0 +1,30 @@ +% Generated by roxygen2: do not edit by hand +% Please edit documentation in R/stream.R +\name{streaming} +\alias{streaming} +\alias{gifski_encoder_init} +\title{Streaming Gifski Encoder} +\usage{ +gifski_encoder_init(gif_file = "animation.gif", width = 800, + height = 600, loop = TRUE, delay = 1) +} +\arguments{ +\item{gif_file}{output gif file} + +\item{width}{gif width in pixels} + +\item{height}{gif height in pixel} + +\item{loop}{should the gif play forever (FALSE to only play once)} + +\item{delay}{time to show each image in seconds} +} +\description{ +Starts a stateful gif encoder and returns a closure to add png frames +to the gif. Upon completion, the caller has to invoke the closure one +more time with `png_file = NULL` to finalize the output. +} +\seealso{ +Other gifski: \code{\link{gifski}} +} +\concept{gifski} diff --git a/src/Makevars.in b/src/Makevars.in index fd66959..e8ad2c6 100644 --- a/src/Makevars.in +++ b/src/Makevars.in @@ -4,7 +4,10 @@ PKG_CFLAGS = -pthread $(C_VISIBILITY) PKG_LIBS = -L$(LIBDIR) -lmyrustlib -lresolv -pthread CARGO = @cargobin@ -#all: clean +# CRAN policy forbids using $HOME +#export CARGO_HOME=$TMPDIR/.cargo + +all: clean $(SHLIB): $(STATLIB) diff --git a/src/init.c b/src/init.c new file mode 100644 index 0000000..80ec7ea --- /dev/null +++ b/src/init.c @@ -0,0 +1,23 @@ +#define R_NO_REMAP +#define STRICT_R_HEADERS +#include <Rinternals.h> +#include <R_ext/Rdynload.h> +#include <R_ext/Visibility.h> + +extern SEXP R_gifski_encoder_add_png(SEXP, SEXP); +extern SEXP R_gifski_encoder_finalize(SEXP); +extern SEXP R_gifski_encoder_new(SEXP, SEXP, SEXP, SEXP); +extern SEXP R_png_to_gif(SEXP, SEXP, SEXP, SEXP, SEXP, SEXP, SEXP); + +static const R_CallMethodDef CallEntries[] = { + {"R_gifski_encoder_add_png", (DL_FUNC) &R_gifski_encoder_add_png, 2}, + {"R_gifski_encoder_finalize", (DL_FUNC) &R_gifski_encoder_finalize, 1}, + {"R_gifski_encoder_new", (DL_FUNC) &R_gifski_encoder_new, 4}, + {"R_png_to_gif", (DL_FUNC) &R_png_to_gif, 7}, + {NULL, NULL, 0} +}; + +attribute_visible void R_init_gifski(DllInfo *dll) { + R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); + R_useDynamicSymbols(dll, FALSE); +} diff --git a/src/streamer.c b/src/streamer.c new file mode 100644 index 0000000..23634cb --- /dev/null +++ b/src/streamer.c @@ -0,0 +1,89 @@ +#define R_NO_REMAP +#define STRICT_R_HEADERS +#include <Rinternals.h> + +// Import C headers for rust API +#include <pthread.h> +#include <string.h> +#include "myrustlib/gifski.h" + +/* data to pass to encoder thread */ +typedef struct { + int i; + char *path; + GifskiError err; + gifski *g; + GifskiSettings settings; + pthread_t encoder_thread; +} gifski_ptr_info; + +/* gifski_write() blocks until main thread calls gifski_end_adding_frames() */ +static void * gifski_encoder_thread(void * data){ + gifski_ptr_info * info = data; + info->err = gifski_write(info->g, info->path); + return NULL; +} + +static void fin_gifski_encoder(SEXP ptr){ + gifski_ptr_info *info = (gifski_ptr_info*) R_ExternalPtrAddr(ptr); + if(info == NULL) + return; + R_SetExternalPtrAddr(ptr, NULL); + if(info->encoder_thread) + pthread_cancel(info->encoder_thread); + gifski_drop(info->g); + free(info->path); + free(info); +} + +static gifski_ptr_info *get_info(SEXP ptr){ + if(TYPEOF(ptr) != EXTPTRSXP || !Rf_inherits(ptr, "gifski_encoder")) + Rf_error("pointer is not a gifski_encoder()"); + if(!R_ExternalPtrAddr(ptr)) + Rf_error("pointer is dead"); + return R_ExternalPtrAddr(ptr); +} + +SEXP R_gifski_encoder_new(SEXP gif_file, SEXP width, SEXP height, SEXP loop){ + gifski_ptr_info *info = malloc(sizeof(gifski_ptr_info)); + info->path = strdup(CHAR(STRING_ELT(gif_file, 0))); + info->settings.height = Rf_asInteger(height); + info->settings.width = Rf_asInteger(width); + info->settings.quality = 100; + info->settings.fast = false; + info->settings.once = !Rf_asLogical(loop); + info->err = GIFSKI_OK; + info->i = 0; + info->g = gifski_new(&info->settings); + if(pthread_create(&info->encoder_thread, NULL, gifski_encoder_thread, info)) + Rf_error("Failed to create encoder thread"); + SEXP ptr = R_MakeExternalPtr(info, R_NilValue, R_NilValue); + Rf_setAttrib(ptr, R_ClassSymbol, Rf_mkString("gifski_encoder")); + R_RegisterCFinalizerEx(ptr, fin_gifski_encoder, TRUE); + return ptr; +} + +SEXP R_gifski_encoder_add_png(SEXP ptr, SEXP png_file, SEXP delay){ + gifski_ptr_info *info = get_info(ptr); + if(info->err != GIFSKI_OK) + Rf_error("Gifski encoder is in bad state"); + const char *path = CHAR(STRING_ELT(png_file, 0)); + int d = Rf_asInteger(delay); + if(gifski_add_frame_png_file(info->g, info->i, path, d) != GIFSKI_OK) + Rf_error("Failed to add frame %d (%s)", info->i, path); + info->i++; + return Rf_ScalarInteger(info->i); +} + +SEXP R_gifski_encoder_finalize(SEXP ptr){ + gifski_ptr_info *info = get_info(ptr); + if(info->err != GIFSKI_OK) + Rf_error("Gifski encoder is in bad state"); + if(gifski_end_adding_frames(info->g) != GIFSKI_OK) + Rf_error("Failed to finalizer encoder"); + pthread_join(info->encoder_thread, NULL); + info->encoder_thread = NULL; + SEXP path = Rf_mkString(info->path); + fin_gifski_encoder(ptr); + return path; +} diff --git a/src/wrapper.c b/src/wrapper.c index 73eab5e..076f110 100644 --- a/src/wrapper.c +++ b/src/wrapper.c @@ -1,8 +1,6 @@ #define R_NO_REMAP #define STRICT_R_HEADERS #include <Rinternals.h> -#include <R_ext/Rdynload.h> -#include <R_ext/Visibility.h> // Import C headers for rust API #include <pthread.h> @@ -69,14 +67,3 @@ SEXP R_png_to_gif(SEXP png_files, SEXP gif_file, SEXP width, SEXP height, SEXP d Rf_error("Failure writing image %s", info.path); return gif_file; } - -// Standard R package stuff -static const R_CallMethodDef CallEntries[] = { - {"R_png_to_gif", (DL_FUNC) &R_png_to_gif, 7}, - {NULL, NULL, 0} -}; - -attribute_visible void R_init_gifski(DllInfo *dll) { - R_registerRoutines(dll, NULL, CallEntries, NULL, NULL); - R_useDynamicSymbols(dll, FALSE); -}