aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--_posts/2018-06-23-slicing-images-gimp-python.md202
-rw-r--r--assets/2018-06-23-slicing-images-gimp-python-1.pngbin0 -> 34303 bytes
-rw-r--r--site.css82
3 files changed, 284 insertions, 0 deletions
diff --git a/_posts/2018-06-23-slicing-images-gimp-python.md b/_posts/2018-06-23-slicing-images-gimp-python.md
new file mode 100644
index 0000000..73ba570
--- /dev/null
+++ b/_posts/2018-06-23-slicing-images-gimp-python.md
@@ -0,0 +1,202 @@
+---
+layout: default
+title: "Slicing and Dicing Images with GIMP and Python"
+---
+
+# {{ page.title }}
+
+Let's say you have one big image (say, a Telegram sticker) and you need to dice it into a bunch of smaller images (say, Discord emoji).
+GIMP can let you do that manually, but frankly so can simpler tools.
+GIMP also has powerful scripting support with Python (and also Scheme, but miss me with that) that can let us do that automatically.
+
+## TL;DR how do i do the thing
+
+1. Save your large image somewhere with a useful filename; this script will chuck `_1_1` and `_1_2` etc on the end of the existing filename.
+2. Open that image in GIMP.
+3. Go to the Filters menu, open Python-Fu, and hit Console.
+4. Set up the width and height of your tiles. For 64x64 tiles, for example, type
+ ```python
+ WIDTH = 64
+ HEIGHT = 64
+ ```
+5. Paste in this big ol' block of code and let it build your tiles and print out the text you can enter in Discord to reconstitute your original image:
+ ```python
+ from gimpfu import *
+ from __future__ import print_function
+ import os.path
+
+ def crop(image, x, width, y, height):
+ pdb.gimp_image_crop(image, width, height, x, y)
+ x_idx = x / width + 1
+ y_idx = y / width + 1
+ filename = pdb.gimp_image_get_filename(image)
+ dir, name = os.path.split(filename)
+ root, ext = os.path.splitext(name)
+ ext = ".png"
+ output_root = root + "_" + str(y_idx) + "_" + str(x_idx)
+ output_name = os.path.join(dir, output_root + ext)
+ layer = pdb.gimp_image_get_active_layer(image)
+ pdb.file_png_save_defaults(image, layer, output_name, output_name)
+ print(":" + output_root + ":", end="")
+ pdb.gimp_image_delete(image)
+
+ image = gimp.image_list()[0]
+ filename = pdb.gimp_image_get_filename(image)
+
+ for y in range(0, 512, WIDTH):
+ for x in range(0, 512, HEIGHT):
+ crop(pdb.gimp_file_load(filename, filename), x, WIDTH, y, HEIGHT)
+ print()
+
+ pass
+ ```
+
+There are two minor issues with actually using this code to convert a Telegram sticker into Discord emoji that I'll get to later.
+
+## The Code, Splained
+
+I'll walk through each bit of the code segment above and explain why it's there.
+
+We need the GIMP libraries, the Python 3 `print()` function (because as of GIMP 2.8.22 the GIMP console is still on Python 2), and some path manipulation functions.
+```python
+from gimpfu import *
+from __future__ import print_function
+import os.path
+```
+
+We're going to crop an image with an X and Y offset and a width and height.
+The first step in generating the tile is telling GIMP to do the actual crop.
+```python
+def crop(image, x, width, y, height):
+ pdb.gimp_image_crop(image, width, height, x, y)
+```
+
+The next step is to figure out the filename for this specific tile; here we're getting an index back from the offsets and width and height.
+```python
+ x_idx = x / width + 1
+ y_idx = y / width + 1
+ filename = pdb.gimp_image_get_filename(image)
+ dir, name = os.path.split(filename)
+ root, ext = os.path.splitext(name)
+ ext = ".png"
+ output_root = root + "_" + str(y_idx) + "_" + str(x_idx)
+ output_name = os.path.join(dir, output_root + ext)
+```
+
+Once we've got a filename, we can save.
+For some reason GIMP's save functions all depend on both the image and the layer, and on two copies of the filename.
+```python
+ layer = pdb.gimp_image_get_active_layer(image)
+ pdb.file_png_save_defaults(image, layer, output_name, output_name)
+```
+
+Since the goal is to reconstitute the original image from Discord emoji, we assume that they won't be renamed.
+We need the Python 3 print function here to suppress any characters after the string is printed; the Python 2 `print "foo",` trick still emits a space.
+```python
+ print(":" + output_root + ":", end="")
+```
+
+We might as well delete the image from GIMP.
+I don't know if this actually serves an important purpose or not.
+```python
+ pdb.gimp_image_delete(image)
+```
+
+We want to grab the original filename.
+```python
+image = gimp.image_list()[0]
+filename = pdb.gimp_image_get_filename(image)
+```
+
+Since we defined WIDTH and HEIGHT manually earlier, now we can loop through the image.
+I should probably go back in and make it grab the full image width and height, but fuck it, I don't want to.
+```python
+for y in range(0, 512, WIDTH):
+ for x in range(0, 512, HEIGHT):
+```
+
+I don't know if GIMP doesn't expose undo in the Python API or if I just couldn't find it, but either way we don't have undo, so we pass in a fresh copy of the image instead.
+```python
+ crop(pdb.gimp_file_load(filename, filename), x, WIDTH, y, HEIGHT)
+```
+
+Since we're building up the emoji text for Discord one row at a time, we need to end the row at the end of a row.
+```python
+ print()
+```
+
+This is just there so the newline after the `for` loop gets pasted successfully.
+```python
+pass
+```
+
+## The Plot Thickens
+
+The first issue with this approach is that Discord (at time of writing, at least) sets a total of 2.25 pixels worth of horizontal margin between emoji, so your reconstituted image will have weird stripes.
+It might be feasible to adjust for these in the offsets so that the spacing isn't funky, but honestly that seems like a lot of work.
+
+The second, and more interesting, issue is that Discord has a 50 emoji limit on each server (at least for non-Nitro plebeians; I don't know if that changes if the server owner upgrades).
+Slicing a 512x512 image into 32x32 tiles for a full size replica would generate 256 tiles, which might work if you had Discord Nitro and six different dummy servers, but nah.
+Slicing into 64x64 tiles that'll be rendered at half size only makes 64 tiles, which works out nicely numerically but is still more than can fit on one server.
+Unless we're clever.
+
+I'm not sure how well this generalizes, but for the sticker I'm working with, 16 of those 64 tiles are fully transparent, and therefore identical.
+If we could detect this when slicing, we could avoid emitting 15 of those, at which point we come in nicely with 49 tiles, one under the Discord emoji limit.
+But how can we detect if an image is fully transparent?
+
+Get histogram info for the alpha channel!
+We can use something like this to count how many pixels aren't fully transparent:
+```python
+_, _, _, _, visible_count, _ = pdb.gimp_histogram(layer, HISTOGRAM_ALPHA, 1, 255)
+```
+
+So our final code can detect if each tile is fully transparent before it saves and treat all fully transparent tiles as equivalent to the very first one.
+
+```python
+from gimpfu import *
+from __future__ import print_function
+import os.path
+
+empty_tile_name = None
+
+def crop(image, x, width, y, height):
+ global empty_tile_name
+ pdb.gimp_image_crop(image, width, height, x, y)
+ layer = pdb.gimp_image_get_active_layer(image)
+ _, _, _, _, visible_count, _ = pdb.gimp_histogram(layer, HISTOGRAM_ALPHA, 1, 255)
+ x_idx = x / width + 1
+ y_idx = y / width + 1
+ filename = pdb.gimp_image_get_filename(image)
+ dir, name = os.path.split(filename)
+ root, ext = os.path.splitext(name)
+ ext = ".png"
+ output_root = root + "_" + str(y_idx) + "_" + str(x_idx)
+ output_name = os.path.join(dir, output_root + ext)
+ if visible_count > 0 or empty_tile_name is None:
+ pdb.file_png_save_defaults(image, layer, output_name, output_name)
+ if visible_count == 0:
+ if empty_tile_name is None:
+ empty_tile_name = output_root
+ else:
+ output_root = empty_tile_name
+ print(":" + output_root + ":", end="")
+ pdb.gimp_image_delete(image)
+
+image = gimp.image_list()[0]
+filename = pdb.gimp_image_get_filename(image)
+
+for y in range(0, 512, WIDTH):
+ for x in range(0, 512, HEIGHT):
+ crop(pdb.gimp_file_load(filename, filename), x, WIDTH, y, HEIGHT)
+ print()
+
+pass
+```
+
+The results are actually fairly impressive, all things considered:
+
+![A halfway decent but slightly stripe-y replica as Discord emoji of the Telegram sticker of Pandora's Fox dabbing.](/assets/2018-06-23-slicing-images-gimp-python-1.png)
+
+(that sticker is by [NL](https://twitter.com/NLDraws) and of [Pandora's Fox](https://twitter.com/pandoras_foxo))
+
+But of course anyone with an ounce of sense would just upload the image so this whole project was a complete waste of three hours. \ No newline at end of file
diff --git a/assets/2018-06-23-slicing-images-gimp-python-1.png b/assets/2018-06-23-slicing-images-gimp-python-1.png
new file mode 100644
index 0000000..290a023
--- /dev/null
+++ b/assets/2018-06-23-slicing-images-gimp-python-1.png
Binary files differ
diff --git a/site.css b/site.css
index 2cf7b9b..ba9cf3b 100644
--- a/site.css
+++ b/site.css
@@ -15,6 +15,18 @@ body {
code {
background: white;
}
+.highlight code {
+ background: unset;
+}
+div.highlighter-rouge {
+ background: white;
+ padding: 1rem;
+ line-height: 1;
+}
+div.highlighter-rouge pre {
+ padding: 0;
+ margin: 0;
+}
a {
border-bottom: 1px solid #444444;
color: #444444;
@@ -23,3 +35,73 @@ a {
a:hover {
border-bottom-style: dashed;
}
+
+/* https://github.com/richleland/pygments-css/blob/master/friendly.css */
+.highlight .hll { background-color: #ffffcc }
+.highlight .c { color: #60a0b0; font-style: italic } /* Comment */
+.highlight .err { border: 1px solid #FF0000 } /* Error */
+.highlight .k { color: #007020; font-weight: bold } /* Keyword */
+.highlight .o { color: #666666 } /* Operator */
+.highlight .ch { color: #60a0b0; font-style: italic } /* Comment.Hashbang */
+.highlight .cm { color: #60a0b0; font-style: italic } /* Comment.Multiline */
+.highlight .cp { color: #007020 } /* Comment.Preproc */
+.highlight .cpf { color: #60a0b0; font-style: italic } /* Comment.PreprocFile */
+.highlight .c1 { color: #60a0b0; font-style: italic } /* Comment.Single */
+.highlight .cs { color: #60a0b0; background-color: #fff0f0 } /* Comment.Special */
+.highlight .gd { color: #A00000 } /* Generic.Deleted */
+.highlight .ge { font-style: italic } /* Generic.Emph */
+.highlight .gr { color: #FF0000 } /* Generic.Error */
+.highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */
+.highlight .gi { color: #00A000 } /* Generic.Inserted */
+.highlight .go { color: #888888 } /* Generic.Output */
+.highlight .gp { color: #c65d09; font-weight: bold } /* Generic.Prompt */
+.highlight .gs { font-weight: bold } /* Generic.Strong */
+.highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
+.highlight .gt { color: #0044DD } /* Generic.Traceback */
+.highlight .kc { color: #007020; font-weight: bold } /* Keyword.Constant */
+.highlight .kd { color: #007020; font-weight: bold } /* Keyword.Declaration */
+.highlight .kn { color: #007020; font-weight: bold } /* Keyword.Namespace */
+.highlight .kp { color: #007020 } /* Keyword.Pseudo */
+.highlight .kr { color: #007020; font-weight: bold } /* Keyword.Reserved */
+.highlight .kt { color: #902000 } /* Keyword.Type */
+.highlight .m { color: #40a070 } /* Literal.Number */
+.highlight .s { color: #4070a0 } /* Literal.String */
+.highlight .na { color: #4070a0 } /* Name.Attribute */
+.highlight .nb { color: #007020 } /* Name.Builtin */
+.highlight .nc { color: #0e84b5; font-weight: bold } /* Name.Class */
+.highlight .no { color: #60add5 } /* Name.Constant */
+.highlight .nd { color: #555555; font-weight: bold } /* Name.Decorator */
+.highlight .ni { color: #d55537; font-weight: bold } /* Name.Entity */
+.highlight .ne { color: #007020 } /* Name.Exception */
+.highlight .nf { color: #06287e } /* Name.Function */
+.highlight .nl { color: #002070; font-weight: bold } /* Name.Label */
+.highlight .nn { color: #0e84b5; font-weight: bold } /* Name.Namespace */
+.highlight .nt { color: #062873; font-weight: bold } /* Name.Tag */
+.highlight .nv { color: #bb60d5 } /* Name.Variable */
+.highlight .ow { color: #007020; font-weight: bold } /* Operator.Word */
+.highlight .w { color: #bbbbbb } /* Text.Whitespace */
+.highlight .mb { color: #40a070 } /* Literal.Number.Bin */
+.highlight .mf { color: #40a070 } /* Literal.Number.Float */
+.highlight .mh { color: #40a070 } /* Literal.Number.Hex */
+.highlight .mi { color: #40a070 } /* Literal.Number.Integer */
+.highlight .mo { color: #40a070 } /* Literal.Number.Oct */
+.highlight .sa { color: #4070a0 } /* Literal.String.Affix */
+.highlight .sb { color: #4070a0 } /* Literal.String.Backtick */
+.highlight .sc { color: #4070a0 } /* Literal.String.Char */
+.highlight .dl { color: #4070a0 } /* Literal.String.Delimiter */
+.highlight .sd { color: #4070a0; font-style: italic } /* Literal.String.Doc */
+.highlight .s2 { color: #4070a0 } /* Literal.String.Double */
+.highlight .se { color: #4070a0; font-weight: bold } /* Literal.String.Escape */
+.highlight .sh { color: #4070a0 } /* Literal.String.Heredoc */
+.highlight .si { color: #70a0d0; font-style: italic } /* Literal.String.Interpol */
+.highlight .sx { color: #c65d09 } /* Literal.String.Other */
+.highlight .sr { color: #235388 } /* Literal.String.Regex */
+.highlight .s1 { color: #4070a0 } /* Literal.String.Single */
+.highlight .ss { color: #517918 } /* Literal.String.Symbol */
+.highlight .bp { color: #007020 } /* Name.Builtin.Pseudo */
+.highlight .fm { color: #06287e } /* Name.Function.Magic */
+.highlight .vc { color: #bb60d5 } /* Name.Variable.Class */
+.highlight .vg { color: #bb60d5 } /* Name.Variable.Global */
+.highlight .vi { color: #bb60d5 } /* Name.Variable.Instance */
+.highlight .vm { color: #bb60d5 } /* Name.Variable.Magic */
+.highlight .il { color: #40a070 } /* Literal.Number.Integer.Long */ \ No newline at end of file