Added argument to freeze for setting the end frame, speed is now aware of variable framerates

This commit is contained in:
TheEssem 2021-04-19 11:04:24 -05:00
parent f21cb21d9c
commit e00671f0d5
5 changed files with 43 additions and 23 deletions

View file

@ -4,6 +4,7 @@
"target_name": "image", "target_name": "image",
"sources": [ "<!@(node -p \"require('fs').readdirSync('./natives').map(f=>'natives/'+f).join(' ')\")" ], "sources": [ "<!@(node -p \"require('fs').readdirSync('./natives').map(f=>'natives/'+f).join(' ')\")" ],
"cflags!": [ "-fno-exceptions", "<!(pkg-config --cflags Magick++)" ], "cflags!": [ "-fno-exceptions", "<!(pkg-config --cflags Magick++)" ],
"cflags_cc": [ "-std=c++17" ],
"cflags_cc!": [ "-fno-exceptions", "<!(pkg-config --cflags Magick++)" ], "cflags_cc!": [ "-fno-exceptions", "<!(pkg-config --cflags Magick++)" ],
"include_dirs": [ "include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")", "<!@(node -p \"require('node-addon-api').include\")",

View file

@ -1,12 +1,17 @@
const ImageCommand = require("../../classes/imageCommand.js"); const ImageCommand = require("../../classes/imageCommand.js");
class FreezeCommand extends ImageCommand { class FreezeCommand extends ImageCommand {
params = { params(args) {
loop: false const frameCount = parseInt(args[0]);
}; return {
loop: false,
frame: isNaN(frameCount) ? -1 : frameCount
};
}
static description = "Makes an image sequence only play once"; static description = "Makes an image sequence only play once";
static aliases = ["noloop", "once"]; static aliases = ["noloop", "once"];
static arguments = ["{end frame number}"];
static requiresGIF = true; static requiresGIF = true;
static noImage = "you need to provide an image to freeze!"; static noImage = "you need to provide an image to freeze!";

View file

@ -7,14 +7,19 @@ using namespace Magick;
class FreezeWorker : public Napi::AsyncWorker { class FreezeWorker : public Napi::AsyncWorker {
public: public:
FreezeWorker(Napi::Function& callback, string in_path, bool loop, string type, int delay) FreezeWorker(Napi::Function& callback, string in_path, bool loop, int frame, string type, int delay)
: Napi::AsyncWorker(callback), in_path(in_path), loop(loop), type(type), delay(delay) {} : Napi::AsyncWorker(callback), in_path(in_path), loop(loop), frame(frame), type(type), delay(delay) {}
~FreezeWorker() {} ~FreezeWorker() {}
void Execute() { void Execute() {
list <Image> frames; list <Image> frames;
readImages(&frames, in_path); readImages(&frames, in_path);
if (frame >= 0 && !loop) {
size_t frameSize = frames.size();
int framePos = clamp(frame, 0, (int)frameSize);
frames.resize(framePos + 1);
}
for_each(frames.begin(), frames.end(), animationIterationsImage(loop ? 0 : 1)); for_each(frames.begin(), frames.end(), animationIterationsImage(loop ? 0 : 1));
for_each(frames.begin(), frames.end(), magickImage(type)); for_each(frames.begin(), frames.end(), magickImage(type));
@ -28,7 +33,7 @@ class FreezeWorker : public Napi::AsyncWorker {
private: private:
string in_path, type; string in_path, type;
int delay; int frame, delay;
Blob blob; Blob blob;
bool loop; bool loop;
}; };
@ -43,8 +48,9 @@ Napi::Value Freeze(const Napi::CallbackInfo &info)
bool loop = obj.Has("loop") ? obj.Get("loop").As<Napi::Boolean>().Value() : false; bool loop = obj.Has("loop") ? obj.Get("loop").As<Napi::Boolean>().Value() : false;
string type = obj.Get("type").As<Napi::String>().Utf8Value(); string type = obj.Get("type").As<Napi::String>().Utf8Value();
int delay = obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0; int delay = obj.Has("delay") ? obj.Get("delay").As<Napi::Number>().Int32Value() : 0;
int frame = obj.Has("frame") ? obj.Get("frame").As<Napi::Number>().Int32Value() : -1;
FreezeWorker* blurWorker = new FreezeWorker(cb, path, loop, type, delay); FreezeWorker* freezeWorker = new FreezeWorker(cb, path, loop, frame, type, delay);
blurWorker->Queue(); freezeWorker->Queue();
return env.Undefined(); return env.Undefined();
} }

View file

@ -15,26 +15,28 @@ class SpeedWorker : public Napi::AsyncWorker {
list <Image> frames; list <Image> frames;
readImages(&frames, in_path); readImages(&frames, in_path);
int old_delay = 0;
// if passed a delay, use that. otherwise use the average frame delay. // if passed a delay, use that. otherwise use the average frame delay.
// GIFs can have a variable framerate, and the frameskipping logic here doesn't handle that.
// TODO: revisit?
if (delay == 0) { if (delay == 0) {
for (Image &image : frames) { for (Image &image : frames) {
old_delay += image.animationDelay(); int old_delay = image.animationDelay();
int new_delay = slow ? old_delay * 2 : old_delay / 2;
if (!slow && new_delay <= 1) {
new_delay = delay;
auto it = frames.begin();
while(it != frames.end() && ++it != frames.end()) it = frames.erase(it);
} else {
image.animationDelay(new_delay);
}
} }
old_delay /= frames.size();
} else { } else {
old_delay = delay; int new_delay = slow ? delay * 2 : delay / 2;
} if (!slow && new_delay <= 1) {
new_delay = delay;
int new_delay = slow ? old_delay * 2 : old_delay / 2; auto it = frames.begin();
if (!slow && new_delay <= 1) { while(it != frames.end() && ++it != frames.end()) it = frames.erase(it);
new_delay = old_delay; } else {
auto it = frames.begin(); for_each(frames.begin(), frames.end(), animationDelayImage(new_delay));
while(it != frames.end() && ++it != frames.end()) it = frames.erase(it); }
} else {
for_each(frames.begin(), frames.end(), animationDelayImage(new_delay));
} }
for_each(frames.begin(), frames.end(), magickImage(type)); for_each(frames.begin(), frames.end(), magickImage(type));

View file

@ -35,6 +35,9 @@ const getImage = async (image, image2, video, gifv = false) => {
if (gifv) { if (gifv) {
const host = new URL(image2).host; const host = new URL(image2).host;
if (tenorURLs.includes(host)) { if (tenorURLs.includes(host)) {
// Tenor doesn't let us access a raw GIF without going through their API,
// so we use that if there's a key in the config and fall back to using the MP4 if there isn't
// Note that MP4 conversion requires an ImageMagick build that supports MPEG decoding
if (process.env.TENOR !== "") { if (process.env.TENOR !== "") {
const data = await fetch(`https://api.tenor.com/v1/gifs?ids=${image2.split("-").pop()}&key=${process.env.TENOR}`); const data = await fetch(`https://api.tenor.com/v1/gifs?ids=${image2.split("-").pop()}&key=${process.env.TENOR}`);
const json = await data.json(); const json = await data.json();
@ -44,10 +47,13 @@ const getImage = async (image, image2, video, gifv = false) => {
payload.delay = (100 / delay.split("/")[0]) * delay.split("/")[1]; payload.delay = (100 / delay.split("/")[0]) * delay.split("/")[1];
} }
} else if (giphyURLs.includes(host)) { } else if (giphyURLs.includes(host)) {
// Can result in an HTML page instead of a GIF
payload.path = `https://media0.giphy.com/media/${image2.split("-").pop()}/giphy.gif`; payload.path = `https://media0.giphy.com/media/${image2.split("-").pop()}/giphy.gif`;
} else if (imgurURLs.includes(host)) { } else if (imgurURLs.includes(host)) {
// Seems that Tenor has a possibility of making GIFs static
payload.path = image.replace(".mp4", ".gif"); payload.path = image.replace(".mp4", ".gif");
} else if (gfycatURLs.includes(host)) { } else if (gfycatURLs.includes(host)) {
// iirc Gfycat also seems to sometimes make GIFs static
payload.path = `https://thumbs.gfycat.com/${image.split("/").pop().split(".mp4")[0]}-size_restricted.gif`; payload.path = `https://thumbs.gfycat.com/${image.split("/").pop().split(".mp4")[0]}-size_restricted.gif`;
} }
payload.type = "image/gif"; payload.type = "image/gif";