From b5425ba4a68cceaa80a63e99c4acc79fbacafb0f Mon Sep 17 00:00:00 2001 From: Essem Date: Sat, 25 Apr 2026 13:22:34 -0500 Subject: [PATCH] feat: Add rotate --- commands/image-editing/rotate.js | 32 ++++++++++++++++++++++++++++++++ commands/image-editing/spin.js | 1 - locales/en-US.json | 9 +++++++++ natives/common/maps.h | 1 + natives/image/commands.h | 1 + natives/image/spin.cc | 27 +++++++++++++++++++-------- 6 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 commands/image-editing/rotate.js diff --git a/commands/image-editing/rotate.js b/commands/image-editing/rotate.js new file mode 100644 index 00000000..254560f9 --- /dev/null +++ b/commands/image-editing/rotate.js @@ -0,0 +1,32 @@ +import MediaCommand from "#cmd-classes/mediaCommand.js"; + +class RotateCommand extends MediaCommand { + paramsFunc() { + const angle = this.getOptionNumber("angle", true); + return { + angle: angle == null || Number.isNaN(angle) ? 90.0 : angle, + }; + } + + static init() { + super.init(); + // required params need to be at the beginning of the array, + // so we use unshift instead of push here + this.flags.unshift({ + name: "angle", + type: "number", + description: "Set the rotation angle", + required: true, + minValue: 1, + maxValue: 360, + }); + return this; + } + + static description = "Rotates an image"; + + static noImage = "You need to provide an image/GIF to rotate!"; + static command = "spin"; +} + +export default RotateCommand; diff --git a/commands/image-editing/spin.js b/commands/image-editing/spin.js index 341bf7c6..81ecb854 100644 --- a/commands/image-editing/spin.js +++ b/commands/image-editing/spin.js @@ -2,7 +2,6 @@ import MediaCommand from "#cmd-classes/mediaCommand.js"; class SpinCommand extends MediaCommand { static description = "Spins an image"; - static aliases = ["rotate"]; static alwaysGIF = true; static noImage = "You need to provide an image/GIF to spin!"; diff --git a/locales/en-US.json b/locales/en-US.json index fbabbbf3..e158568f 100644 --- a/locales/en-US.json +++ b/locales/en-US.json @@ -64,6 +64,7 @@ "pixelate": "Pixelates an image", "reddit": "Adds a Reddit watermark to an image", "reverse": "Reverses an image sequence", + "rotate": "Rotates an image", "scott": "Makes Scott the Woz show off an image", "sepia": "Adds a sepia filter", "sharpen": "Sharpens an image", @@ -203,6 +204,9 @@ "motivate": { "font": "font" }, + "rotate": { + "angle": "angle" + }, "slide": { "reverse": "reverse", "vertical": "vertical" @@ -425,6 +429,9 @@ "motivate": { "font": "Specify the font you want to use (default: times)" }, + "rotate": { + "angle": "Set the rotation angle" + }, "slide": { "reverse": "Changes the direction the image slides in", "vertical": "Slides the image vertically" @@ -600,6 +607,7 @@ "pixelate": "pixelate", "reddit": "reddit", "reverse": "reverse", + "rotate": "rotate", "scott": "scott", "sepia": "sepia", "sharpen": "sharpen", @@ -668,6 +676,7 @@ "pixelate": "You need to provide an image/GIF to pixelate!", "watermark powerdirector": "You need to provide an image/GIF to add a PowerDirector watermark!", "reverse": "You need to provide an image/GIF to reverse!", + "rotate": "You need to provide an image/GIF to rotate!", "scott": "You need to provide an image/GIF for Scott to show off!", "sepia": "You need to provide an image/GIF to add a sepia filter!", "sharpen": "You need to provide an image/GIF to sharpen!", diff --git a/natives/common/maps.h b/natives/common/maps.h index aee70b2e..2701938d 100644 --- a/natives/common/maps.h +++ b/natives/common/maps.h @@ -90,6 +90,7 @@ namespace esmb { {"snapchat", &SnapchatArgs }, {"sonic", &SonicArgs }, {"speed", &SpeedArgs }, + {"spin", &SpinArgs }, {"spotify", &SpotifyArgs }, {"uncanny", &UncannyArgs }, {"uncaption", &UncaptionArgs }, diff --git a/natives/image/commands.h b/natives/image/commands.h index dd798432..a0194900 100644 --- a/natives/image/commands.h +++ b/natives/image/commands.h @@ -85,6 +85,7 @@ namespace esmb::Image { declare_input_args(SnapchatArgs); declare_input_args(SonicArgs); declare_input_args(SpeedArgs); + declare_input_args(SpinArgs); declare_input_args(SpotifyArgs); declare_input_args(UncannyArgs); declare_input_args(UncaptionArgs); diff --git a/natives/image/spin.cc b/natives/image/spin.cc index 57e2734c..5762b6b3 100644 --- a/natives/image/spin.cc +++ b/natives/image/spin.cc @@ -5,8 +5,14 @@ using namespace std; using namespace vips; +FunctionArgs esmb::Image::SpinArgs = { + {"angle", {typeid(int), false}} +}; + CmdOutput esmb::Image::Spin(const string &type, string &outType, const char *bufferdata, size_t bufferLength, [[maybe_unused]] esmb::ArgumentMap arguments, bool *shouldKill) { + int staticAngle = GetArgumentWithFallback(arguments, "angle", 0); + VImage in = VImage::new_from_buffer(bufferdata, bufferLength, "", GetInputOptions(type, true, true)); int width = in.width(); @@ -25,26 +31,31 @@ CmdOutput esmb::Image::Spin(const string &type, string &outType, const char *buf if (nPages == 1) { multiPage = false; - nPages = 30; + if (staticAngle == 0) { + nPages = 30; + } } vector img; + int outPageHeight = pageHeight; for (int i = 0; i < nPages; i++) { VImage img_frame = multiPage ? in.crop(0, i * pageHeight, width, pageHeight) : in; - double rotation = (double)360 * i / nPages; - VImage rotated = img_frame.similarity(VImage::option()->set("angle", rotation)); - VImage embedded = - rotated.embed((width / 2) - (rotated.width() / 2), (pageHeight / 2) - (rotated.height() / 2), width, pageHeight); + double rotation = staticAngle > 0 ? staticAngle : (double)360 * i / nPages; + VImage rotated = img_frame.rotate(rotation); + VImage embedded = staticAngle > 0 ? rotated + : rotated.embed((width / 2) - (rotated.width() / 2), + (pageHeight / 2) - (rotated.height() / 2), width, pageHeight); + outPageHeight = embedded.height(); img.push_back(embedded); } VImage final = VImage::arrayjoin(img, VImage::option()->set("across", 1)); - final.set(VIPS_META_PAGE_HEIGHT, pageHeight); - if (!multiPage) { + final.set(VIPS_META_PAGE_HEIGHT, outPageHeight); + if (!multiPage && staticAngle == 0) { vector delay(30, 50); final.set("delay", delay); } - if (outType != "webp") outType = "gif"; + if (outType != "webp" && staticAngle == 0) outType = "gif"; SetupTimeoutCallback(final, shouldKill); -- 2.51.2