1313# You should have received a copy of the GNU General Public License
1414# along with this program; if not, write to the Free Software
1515# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
16+ from collections import namedtuple
17+
1618from PIL import Image
1719from PIL import ImageFilter
1820from PIL import ImageDraw
@@ -61,12 +63,44 @@ def nextPowerOf2(n):
6163
6264 return 1 << count ;
6365
66+
67+ def get_cropspec (image , corners , rotation ):
68+ t , l , r , b = corners
69+ w = r - l
70+ h = b - t
71+
72+ # The coordinates passed to jpegtran are interpreted post-rotation.
73+ # Non-whole blocks are already imperfectly rotated by being left on the
74+ # side, so we need to subtract them
75+ if image .format == "JPEG" :
76+ round_x , round_y = image_round (image )
77+ orig_w , orig_h = image .size
78+ if rotation in (8 , 6 ):
79+ orig_w , orig_h = orig_h , orig_w
80+ round_x , round_y = round_y , round_x
81+ if rotation in (3 , 8 ):
82+ t -= orig_h % round_y
83+ if rotation in (3 , 6 ):
84+ l -= orig_w % round_x
85+ assert t >= 0 , "t < 0 should be handled in fix(): {}" .format (t )
86+ assert l >= 0 , "l < 0 should be handled in fix(): {}" .format (l )
87+
88+ return "%dx%d+%d+%d" % (w , h , l , t )
89+
90+
6491def ncpus ():
6592 if os .path .exists ("/proc/cpuinfo" ):
6693 return open ("/proc/cpuinfo" ).read ().count ("bogomips" ) or 1
6794 return 1
6895ncpus = ncpus ()
6996
97+
98+ CropRequest = namedtuple (
99+ "CropRequest" ,
100+ ["image" , "image_name" , "corners" , "rotation" , "target" ]
101+ )
102+
103+
70104class CropTask (object ):
71105 def __init__ (self , log ):
72106 self .log = log
@@ -86,17 +120,55 @@ def create_task(self):
86120 def count (self ):
87121 return len (self .tasks ) + len (self .threads )
88122
89- def add (self , args , target ):
90- self .tasks .put (( args , target ) )
123+ def add (self , task ):
124+ self .tasks .put (task )
91125
92126 def runner (self ):
93127 while 1 :
94128 task = self .tasks .get ()
95129 if task is None :
96130 break
97- command , target = task
131+ image = task .image
132+ image_name = task .image_name
133+ rotation_int = task .rotation
134+ target = task .target
98135 shortname = os .path .basename (target )
99136 self .log .progress (_ ("Cropping to %s" ) % shortname )
137+
138+ t , l , r , b = task .corners
139+ cropspec = get_cropspec (image , task .corners , rotation_int )
140+
141+ if rotation_int == 3 :
142+ rotation = "180"
143+ elif rotation_int == 6 :
144+ rotation = "90"
145+ elif rotation_int == 8 :
146+ rotation = "270"
147+ else :
148+ rotation = "none"
149+
150+ # Copy file if no cropping or rotation.
151+ if (r + b - l - t ) == (image .width + image .height ) and rotation == "none" :
152+ command = ["nice" , "cp" , image_name , target ]
153+ # JPEG crop uses jpegtran
154+ elif image .format == "JPEG" :
155+ command = ["nice" , "jpegtran" ]
156+ if rotation != "none" :
157+ command += ["-rotate" , rotation ]
158+ command += [
159+ "-copy" , "all" ,
160+ "-crop" , cropspec ,
161+ "-outfile" , target ,
162+ image_name ,
163+ ]
164+ # All other images use ImageMagick convert.
165+ else :
166+ command = ["nice" , "convert" ]
167+ if rotation != "none" :
168+ command += ["-rotate" , rotation ]
169+ command += [image_name , "-crop" , cropspec , target ]
170+
171+ print (" " .join (command ))
100172 subprocess .call (command )
101173 subprocess .call (["exiftool" , "-overwrite_original" , "-Orientation=1" , "-n" , target ])
102174 self .log .log (_ ("Cropped to %s" ) % shortname )
@@ -106,7 +178,8 @@ def __init__(self):
106178 self .render_flag = 0
107179 self .show_handles = True
108180 self .state = DRAG_NONE
109- self .round = 8
181+ self .round_x = None
182+ self .round_y = None
110183 self .image = None
111184 self .w = 0
112185 self .h = 0
@@ -133,10 +206,8 @@ def apply_rotation(self, image):
133206 def image_or_rotation_changed (self ):
134207 self ._image = image = self .apply_rotation (self ._orig_image )
135208 self .apply_rotation (image )
136- self .top = 0
137- self .left = 0
138- self .right = self .w
139- self .bottom = self .h
209+ self .top , self .bottom = self .fix (0 , self .h , self .h , self .round_y , self .rotation in (3 , 8 ))
210+ self .left , self .right = self .fix (0 , self .w , self .w , self .round_x , self .rotation in (3 , 6 ))
140211 blurred = image .copy ()
141212 mult = len (self .image .mode ) # replicate filter for L, RGB, RGBA
142213 self .blurred = image .copy ().filter (
@@ -145,20 +216,33 @@ def image_or_rotation_changed(self):
145216 self .image_set ()
146217 self .render ()
147218
148- def fix (self , a , b , lim ):
149- a , b = sorted ((b ,a ))
150- a = clamp (a , 0 , lim )
151- b = clamp (b , 0 , lim )
152- a = (a / self .round )* self .round
153- b = (b / self .round )* self .round
154- return int (a ), int (b )
219+ def fix (self , a , b , lim , r , reverse ):
220+ """
221+ a, b: interval to fix
222+ lim: upper bound
223+ r: rounding size
224+ reverse: True to treat the upper bound as the origin
225+ """
226+ if reverse :
227+ offset = lim % r
228+ else :
229+ offset = 0
230+ a , b = sorted ((int (a ), int (b )))
231+ a = ((a - offset ) // r ) * r + offset
232+ b = ((b - offset + r - 1 ) // r ) * r + offset
233+ # jpegtran handles non-whole blocks by leaving them on the edge of the
234+ # image, away from the rotated position of their old neighbors.
235+ # Keeping them isn't useful, so clamp them off.
236+ a = clamp (a , offset if reverse else 0 , lim )
237+ b = clamp (b , offset if reverse else 0 , lim )
238+ return a , b
155239
156240 def get_corners (self ):
157241 return self .top , self .left , self .right , self .bottom
158242
159243 def get_screencorners (self ):
160244 t , l , r , b = self .get_corners ()
161- return (int (t / int (self .scale )), int (l / int (self .scale )),
245+ return (int (t / int (self .scale )), int (l / int (self .scale )),
162246 int (r / int (self .scale )), int (b / int (self .scale )))
163247
164248 def describe_ratio (self ):
@@ -198,8 +282,8 @@ def set_stdsize(self, x, y):
198282 self .set_crop (top , left , right , bottom )
199283
200284 def set_crop (self , top , left , right , bottom ):
201- self .top , self .bottom = self .fix (top , bottom , self .h )
202- self .left , self .right = self .fix (left , right , self .w )
285+ self .top , self .bottom = self .fix (top , bottom , self .h , self . round_y , self . rotation in ( 3 , 8 ) )
286+ self .left , self .right = self .fix (left , right , self .w , self . round_x , self . rotation in ( 3 , 6 ) )
203287 self .render ()
204288
205289 def get_image (self ):
@@ -332,8 +416,12 @@ def drag_end(self, x, y):
332416 self .set_crop (self .top , self .left , self .right , self .bottom )
333417 self .state = DRAG_NONE
334418
335- def rotate_ccw (self ):
419+ def _flip_dimensions (self ):
336420 self .w , self .h = self .h , self .w
421+ self .round_x , self .round_y = self .round_y , self .round_x
422+
423+ def rotate_ccw (self ):
424+ self ._flip_dimensions ()
337425 r = self .rotation
338426 if r == 1 : r = 8
339427 elif r == 8 : r = 3
@@ -342,7 +430,7 @@ def rotate_ccw(self):
342430 self .rotation = r
343431
344432 def rotate_cw (self ):
345- self .w , self . h = self . h , self . w
433+ self ._flip_dimensions ()
346434 r = self .rotation
347435 if r == 1 : r = 6
348436 elif r == 6 : r = 3
@@ -377,6 +465,17 @@ def image_rotation(i):
377465 print ("image_rotation" , result )
378466 return result or 1
379467
468+
469+ def image_round (i ):
470+ """Return (horizontal block size, vertical block size)"""
471+ if i .format == "JPEG" :
472+ x = max (xsamp for _id , xsamp , ysamp , _qtable in i .layer )
473+ y = max (ysamp for _id , xsamp , ysamp , _qtable in i .layer )
474+ return x * 8 , y * 8
475+ else :
476+ return 1 , 1
477+
478+
380479_desktop_name = None
381480def desktop_name ():
382481 global _desktop_name
0 commit comments