1#!/usr/local/bin/python
2
3#   Gimp-Python - allows the writing of Gimp plugins in Python.
4#   Copyright (C) 1997  James Henstridge <james@daa.com.au>
5#
6#   This program is free software: you can redistribute it and/or modify
7#   it under the terms of the GNU General Public License as published by
8#   the Free Software Foundation; either version 3 of the License, or
9#   (at your option) any later version.
10#
11#   This program is distributed in the hope that it will be useful,
12#   but WITHOUT ANY WARRANTY; without even the implied warranty of
13#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14#   GNU General Public License for more details.
15#
16#   You should have received a copy of the GNU General Public License
17#   along with this program.  If not, see <https://www.gnu.org/licenses/>.
18
19# Algorithms stolen from the whirl and pinch plugin distributed with Gimp,
20# by Federico Mena Quintero and Scott Goehring
21#
22# This version does the same thing, except there is no preview, and it is
23# written in python and is slower.
24
25import math, struct
26from gimpfu import *
27
28class pixel_fetcher:
29        def __init__(self, drawable):
30                self.col = -1
31                self.row = -1
32                self.img_width = drawable.width
33                self.img_height = drawable.height
34                self.img_bpp = drawable.bpp
35                self.img_has_alpha = drawable.has_alpha
36                self.tile_width = gimp.tile_width()
37                self.tile_height = gimp.tile_height()
38                self.bg_colour = '\0\0\0\0'
39                self.bounds = drawable.mask_bounds
40                self.drawable = drawable
41                self.tile = None
42        def set_bg_colour(self, r, g, b, a):
43                self.bg_colour = struct.pack('BBB', r,g,b)
44                if self.img_has_alpha:
45                        self.bg_colour = self.bg_colour + chr(a)
46        def get_pixel(self, x, y):
47                sel_x1, sel_y1, sel_x2, sel_y2 = self.bounds
48                if x < sel_x1 or x >= sel_x2 or y < sel_y1 or y >= sel_y2:
49                        return self.bg_colour
50                col = x / self.tile_width
51                coloff = x % self.tile_width
52                row = y / self.tile_height
53                rowoff = y % self.tile_height
54
55                if col != self.col or row != self.row or self.tile == None:
56                        self.tile = self.drawable.get_tile(False, row, col)
57                        self.col = col
58                        self.row = row
59                return self.tile[coloff, rowoff]
60
61class Dummy:
62        pass
63
64def whirl_pinch(image, drawable, whirl, pinch, radius):
65        self = Dummy()
66        self.width = drawable.width
67        self.height = drawable.height
68        self.bpp = drawable.bpp
69        self.has_alpha = drawable.has_alpha
70        self.bounds = drawable.mask_bounds
71        self.sel_x1, self.sel_y1, self.sel_x2, self.sel_y2 = \
72                     drawable.mask_bounds
73        self.sel_w = self.sel_x2 - self.sel_x1
74        self.sel_h = self.sel_y2 - self.sel_y1
75        self.cen_x = (self.sel_x1 + self.sel_x2 - 1) / 2.0
76        self.cen_y = (self.sel_y1 + self.sel_y2 - 1) / 2.0
77        xhsiz = (self.sel_w - 1) / 2.0
78        yhsiz = (self.sel_h - 1) / 2.0
79
80        if xhsiz < yhsiz:
81                self.scale_x = yhsiz / xhsiz
82                self.scale_y = 1.0
83        elif xhsiz > yhsiz:
84                self.scale_x = 1.0
85                self.scale_y = xhsiz / yhsiz
86        else:
87                self.scale_x = 1.0
88                self.scale_y = 1.0
89
90        self.radius = max(xhsiz, yhsiz);
91
92        if not drawable.is_rgb and not drawable.is_grey:
93                return
94
95        gimp.tile_cache_ntiles(2 * (1 + self.width / gimp.tile_width()))
96
97        whirl = whirl * math.pi / 180
98        dest_rgn = drawable.get_pixel_rgn(self.sel_x1, self.sel_y1,
99                                          self.sel_w, self.sel_h, True, True)
100        pft = pixel_fetcher(drawable)
101        pfb = pixel_fetcher(drawable)
102
103        bg_colour = gimp.get_background()
104
105        pft.set_bg_colour(bg_colour[0], bg_colour[1], bg_colour[2], 0)
106        pfb.set_bg_colour(bg_colour[0], bg_colour[1], bg_colour[2], 0)
107
108        progress = 0
109        max_progress = self.sel_w * self.sel_h
110
111        gimp.progress_init("Whirling and pinching")
112
113        self.radius2 = self.radius * self.radius * radius
114        pixel = ['', '', '', '']
115        values = [0,0,0,0]
116
117        for row in range(self.sel_y1, (self.sel_y1+self.sel_y2)/2+1):
118                top_p = ''
119                bot_p = ''
120                for col in range(self.sel_x1, self.sel_x2):
121                        q, cx, cy = calc_undistorted_coords(self, col,
122                                                            row, whirl, pinch,
123                                                            radius)
124                        if q:
125                                if cx >= 0: ix = int(cx)
126                                else:       ix = -(int(-cx) + 1)
127                                if cy >= 0: iy = int(cy)
128                                else:       iy = -(int(-cy) + 1)
129                                pixel[0] = pft.get_pixel(ix, iy)
130                                pixel[1] = pft.get_pixel(ix+1, iy)
131                                pixel[2] = pft.get_pixel(ix, iy+1)
132                                pixel[3] = pft.get_pixel(ix+1, iy+1)
133                                for i in range(self.bpp):
134                                        values[0] = ord(pixel[0][i])
135                                        values[1] = ord(pixel[1][i])
136                                        values[2] = ord(pixel[2][i])
137                                        values[3] = ord(pixel[3][i])
138                                        top_p = top_p + bilinear(cx,cy, values)
139                                cx = self.cen_x + (self.cen_x - cx)
140                                cy = self.cen_y + (self.cen_y - cy)
141                                if cx >= 0: ix = int(cx)
142                                else:       ix = -(int(-cx) + 1)
143                                if cy >= 0: iy = int(cy)
144                                else:       iy = -(int(-cy) + 1)
145                                pixel[0] = pfb.get_pixel(ix, iy)
146                                pixel[1] = pfb.get_pixel(ix+1, iy)
147                                pixel[2] = pfb.get_pixel(ix, iy+1)
148                                pixel[3] = pfb.get_pixel(ix+1, iy+1)
149                                tmp = ''
150                                for i in range(self.bpp):
151                                        values[0] = ord(pixel[0][i])
152                                        values[1] = ord(pixel[1][i])
153                                        values[2] = ord(pixel[2][i])
154                                        values[3] = ord(pixel[3][i])
155                                        tmp = tmp + bilinear(cx,cy, values)
156                                bot_p = tmp + bot_p
157                        else:
158                                top_p = top_p + pft.get_pixel(col, row)
159                                bot_p = pfb.get_pixel((self.sel_x2 - 1) -
160                                        (col - self.sel_x1), (self.sel_y2-1) -
161                                        (row - self.sel_y1)) + bot_p
162
163                dest_rgn[self.sel_x1:self.sel_x2, row] = top_p
164                dest_rgn[self.sel_x1:self.sel_x2, (self.sel_y2 - 1)
165                         - (row - self.sel_y1)] = bot_p
166
167                progress = progress + self.sel_w * 2
168                gimp.progress_update(float(progress) / max_progress)
169
170        drawable.flush()
171        drawable.merge_shadow(True)
172        drawable.update(self.sel_x1,self.sel_y1,self.sel_w,self.sel_h)
173
174def calc_undistorted_coords(self, wx, wy, whirl, pinch, radius):
175        dx = (wx - self.cen_x) * self.scale_x
176        dy = (wy - self.cen_y) * self.scale_y
177        d = dx * dx + dy * dy
178        inside = d < self.radius2
179
180        if inside:
181                dist = math.sqrt(d / radius) / self.radius
182                if (d == 0.0):
183                        factor = 1.0
184                else:
185                        factor = math.pow(math.sin(math.pi / 2 * dist),
186                                          -pinch)
187                dx = dx * factor
188                dy = dy * factor
189                factor = 1 - dist
190                ang = whirl * factor * factor
191                sina = math.sin(ang)
192                cosa = math.cos(ang)
193                x = (cosa * dx - sina * dy) / self.scale_x + self.cen_x
194                y = (sina * dx + cosa * dy) / self.scale_y + self.cen_y
195        else:
196                x = wx
197                y = wy
198        return inside, float(x), float(y)
199
200def bilinear(x, y, values):
201        x = x % 1.0
202        y = y % 1.0
203        m0 = values[0] + x * (values[1] - values[0])
204        m1 = values[2] + x * (values[3] - values[2])
205        return chr(int(m0 + y * (m1 - m0)))
206
207
208register(
209        "python-fu-whirl-pinch",
210        "Distorts an image by whirling and pinching",
211        "Distorts an image by whirling and pinching",
212        "James Henstridge (translated from C plugin)",
213        "James Henstridge",
214        "1997-1999",
215        "_Whirl and Pinch...",
216        "RGB*, GRAY*",
217        [
218            (PF_IMAGE, "image", "Input image", None),
219            (PF_DRAWABLE, "drawable", "Input drawable", None),
220            (PF_SLIDER, "whirl", "Whirl angle", 90, (-360, 360, 1)),
221            (PF_FLOAT, "pinch", "Pinch amount", 0),
222            (PF_FLOAT, "radius", "radius", 1)
223        ],
224        [],
225        whirl_pinch, menu="<Image>/Filters/Distorts")
226
227main()
228