Jelajahi Sumber

Add seychelles command

Kirk Trombley 4 tahun lalu
induk
melakukan
4211450a0d
4 mengubah file dengan 202 tambahan dan 1 penghapusan
  1. 0 1
      README.md
  2. 1 0
      requirements.txt
  3. 200 0
      src/commands/seychelles.py
  4. 1 0
      src/setup.py

+ 0 - 1
README.md

@@ -9,7 +9,6 @@ TODO as always
 ## Missing Commands
 
 Needs the following ported from rollbot3 for feature parity:
-- seychelles
 - session
 - curse
 - hangguy

+ 1 - 0
requirements.txt

@@ -22,6 +22,7 @@ mccabe==0.6.1
 multidict==5.1.0
 mypy-extensions==0.4.3
 pathspec==0.8.1
+Pillow==8.2.0
 pydantic==1.8.2
 pyparsing==2.4.7
 regex==2021.4.4

+ 200 - 0
src/commands/seychelles.py

@@ -0,0 +1,200 @@
+from urllib.request import urlretrieve
+from logging import Logger
+import math
+import os
+
+from PIL import Image
+
+from rollbot import as_command, RollbotFailure, Attachment
+from rollbot.injection import Args
+
+
+@as_command
+def seychelles(argstr: Args, logger: Logger):
+    if "i.imgur.com" not in argstr:
+        RollbotFailure.INVALID_ARGUMENTS.raise_exc(
+            detail="The !seychelles command requires a direct imgur link argument."
+        )
+
+    try:
+        urlretrieve(argstr, "/tmp/seychelles")
+    except:
+        logger.exception("Image download failed")
+        RollbotFailure.SERVICE_DOWN.raise_exc(detail="Couldn't download the provided image")
+
+    s = Seychelles(logger, "/tmp/seychelles", name_out="/tmp/seychelles_out", ext_out="png")
+    s.seychelles(verbose=True)
+    s.save()
+
+    with open("/tmp/seychelles_out.png", "rb") as s_out:
+        return Attachment(name="image", body=s_out.read())
+
+
+# By Akshay Chitale for r/vexillology on Reddit
+# With small modifications to replace print statements
+
+# For Python 3
+
+
+class Seychelles:
+    def __init__(self, logger, name_in, size_out=None, name_out=None, ext_out=None):
+        self.logger = logger
+        # Set up input
+        self.name_in, self.ext_in = os.path.splitext(name_in)
+        self.img_raw = Image.open(name_in)
+        self.img_raw = self.img_raw.convert("RGB")
+        self.size_in = self.img_raw.size
+        # Flip so that colors are measured from top left
+        self.img_in = self.img_raw.transpose(Image.FLIP_TOP_BOTTOM)
+
+        # Set up output
+        self.size_out = size_out if size_out else self.size_in
+        self.name_out = name_out if name_out else self.name_in + "_out"
+        self.ext_out = "." + ext_out if ext_out else self.ext_in
+        self.img_out = Image.new("RGB", self.size_out)
+        self.pixels_out = self.img_out.load()
+
+        # Set up image to print
+        self.img_print = None
+
+    def _angle_transfer(self, diagonal, seychelles):
+        # Define transfer curve as a parabola
+        x1, y1 = 0, 0
+        x2, y2 = (diagonal, math.pi / 4)
+        x3, y3 = math.pi / 2, math.pi / 2
+        # Below code from http://chris35wills.github.io/parabola_python/
+        denom = (x1 - x2) * (x1 - x3) * (x2 - x3)
+        A = (x3 * (y2 - y1) + x2 * (y1 - y3) + x1 * (y3 - y2)) / denom
+        B = (x3 * x3 * (y1 - y2) + x2 * x2 * (y3 - y1) + x1 * x1 * (y2 - y3)) / denom
+        # C = 0 guaranteed because parabola goes through (0,0)
+        if seychelles:
+            # Going forward, use parabola
+            return lambda x: A * x * x + B * x
+        else:
+            # Inverse, solve parabola y=ax^2+bx
+            # Watch out for squares, where diagonal is pi/4 so A=0
+            if abs(diagonal - math.pi / 4) < 1e-9:
+                return lambda x: x
+            else:
+                return lambda x: (-1 * B + math.sqrt(B * B - 4 * A * (-x))) / (2 * A)
+
+    def seychelles(self, verbose=False):
+        # Diagonal angle of output image
+        out_diagonal = math.atan2(self.size_out[1], self.size_out[0])
+        angle_transfer = self._angle_transfer(out_diagonal, True)
+        # Find output color for each output pixel
+        if verbose:
+            self.logger.info(" Progress:   0%")
+        for x in range(self.size_out[0]):
+            if verbose:
+                self.logger.info(
+                    "\r Progress: " + str(int(100 * x / self.size_out[0])).rjust(3) + "%"
+                )
+            for y in range(self.size_out[1]):
+                # First, get the angle
+                out_angle = math.atan2(y, x)
+
+                # Then, follow the vector to the end of the flag
+                out_x, out_y = x, y
+                if x == 0 and y == 0:
+                    out_x, out_y = 1.0, 1.0
+                elif out_angle < out_diagonal:
+                    # Scale by x
+                    out_x *= self.size_out[0] * 1.0 / x
+                    out_y *= self.size_out[0] * 1.0 / x
+                else:
+                    # Scale by y
+                    out_x *= self.size_out[1] * 1.0 / y
+                    out_y *= self.size_out[1] * 1.0 / y
+
+                # Get ratio of point radius to full radius
+                point_rad = math.sqrt(x * x + y * y)
+                out_rad = math.sqrt(out_x * out_x + out_y * out_y)
+                rad_ratio = point_rad / out_rad
+
+                # Coordinates on the input are:
+                # x = radius, scaled by width of input
+                in_x = rad_ratio * self.size_in[0]
+                # y = angle, scaled by height of input
+                in_y = angle_transfer(out_angle) * self.size_in[1] * 2.0 / math.pi
+
+                # Ensure coordinates are within range
+                in_x_int = int(round(in_x))
+                if in_x_int < 0:
+                    in_x_int = 0
+                elif in_x_int >= self.size_in[0]:
+                    in_x_int = self.size_in[0] - 1
+                in_y_int = int(round(in_y))
+                if in_y_int < 0:
+                    in_y_int = 0
+                elif in_y_int >= self.size_in[1]:
+                    in_y_int = self.size_in[1] - 1
+
+                # Assign input color to output color
+                self.pixels_out[x, y] = self.img_in.getpixel((in_x_int, in_y_int))
+        if verbose:
+            self.logger.info("\r Progress: 100%")
+        # Flip so that seychelles is from bottom left
+        self.img_print = self.img_out.transpose(Image.FLIP_TOP_BOTTOM)
+
+    def inverse_seychelles(self, verbose=False):
+        # Diagonal angle of input image
+        in_diagonal = math.atan2(self.size_in[1], self.size_in[0])
+        angle_transfer = self._angle_transfer(in_diagonal, False)
+        # Find output color for each output pixel
+        if verbose:
+            self.logger.info(" Progress:   0%")
+        for x in range(self.size_out[0]):
+            if verbose:
+                self.logger.info(
+                    "\r Progress: " + str(int(100 * x / self.size_out[0])).rjust(3) + "%"
+                )
+            for y in range(self.size_out[1]):
+                # First, get the angle
+                in_angle = angle_transfer(y * math.pi / 2.0 / self.size_out[1])
+
+                # Get ratio of point to full width
+                rad_ratio = x * 1.0 / self.size_out[0]
+
+                # Then, follow the vector to the end of the flag
+                if in_angle < in_diagonal:
+                    # Find by x
+                    in_x = self.size_in[0] * 1.0
+                    in_y = in_x * math.tan(in_angle)
+                else:
+                    # Find by y
+                    in_y = self.size_in[1] * 1.0
+                    in_x = in_y / math.tan(in_angle)
+                # Scale by radius ratio
+                in_x, in_y = rad_ratio * in_x, rad_ratio * in_y
+
+                # Ensure coordinates are within range
+                in_x_int = int(round(in_x))
+                if in_x_int < 0:
+                    in_x_int = 0
+                elif in_x_int >= self.size_in[0]:
+                    in_x_int = self.size_in[0] - 1
+                in_y_int = int(round(in_y))
+                if in_y_int < 0:
+                    in_y_int = 0
+                elif in_y_int >= self.size_in[1]:
+                    in_y_int = self.size_in[1] - 1
+
+                # Assign input color to output color
+                self.pixels_out[x, y] = self.img_in.getpixel((in_x_int, in_y_int))
+        if verbose:
+            self.logger.info("\r Progress: 100%")
+        # Flip so that seychelles is from bottom left
+        self.img_print = self.img_out.transpose(Image.FLIP_TOP_BOTTOM)
+
+    def save(self, name_out=None, ext_out=None):
+        if self.img_print is None:
+            raise Exception("No processing done yet")
+        name = name_out if name_out else self.name_out
+        ext = "." + ext_out if ext_out else self.ext_out
+        self.img_print.save(name + ext)
+
+    def show(self):
+        if self.img_print is None:
+            raise Exception("No processing done yet")
+        self.img_print.show()

+ 1 - 0
src/setup.py

@@ -14,5 +14,6 @@ setup(
         "dice",
         "bs4",
         "gtts",
+        "pillow",
     ],
 )